公网服务器或客户端为 NAT 网络的服务器不要同时开启 tcp_tw_recycle 和 tcp_timestamps

目录

    • 目录
    • 背景及现象
    • 问题原因
      • 结论
    • 实践
      • 如何配置 tcp_tw_recycle 和 tcp_timestamps
      • 为什么很多时候开启了 tcp_tw_recycle 也并没有产生问题

背景及现象

服务器:公网服务器
客户端:外网手游客户端
玩家侧现象:大批处于同一局域网内的内侧玩家连接服务器超时,但是切换成 4G 之后连接顺畅。
服务端现象:netstat -s" 显示 “passive connections rejected because of time stamp” 数量增长快速,关闭 tcp_tw_recycletcp_timestamps 恢复正常,玩家侧现象消失。

问题原因

tcp_tw_recycle 依赖于 tcp_timestamps, 前者默认关闭,后者默认开启。如果二者都开启,当服务器收到 FIN 包时,这个 FIN 包中的时间戳值会按 IP 缓存起来(缓存 TCP_PAWS_MSL, 即 60s),缓存时间内,当同一 IP 的 SYN 包时间戳小于该值,则直接丢弃。代码分析如下:

tcp_v4_conn_request()

/* VJ's idea. We save last timestamp seen
 * from the destination in peer table, when entering
 * state TIME-WAIT, and check against it before
 * accepting new connection request.
 *
 * If "isn" is not zero, this request hit alive
 * timewait bucket, so that all the necessary checks
 * are made in the function processing timewait state.
 */
if (tmp_opt.saw_tstamp &&
    tcp_death_row.sysctl_tw_recycle &&
    (dst = inet_csk_route_req(sk, req)) != NULL &&
    (peer = rt_get_peer((struct rtable *)dst)) != NULL &&
    peer->v4daddr == saddr) {
        if ((u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL &&
            (s32)(peer->tcp_ts - req->ts_recent) >
                                        TCP_PAWS_WINDOW) {
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSPASSIVEREJECTED);
                goto drop_and_release;
        }
}

由于 peer->tcp_ts_stamp 记录的是之前收到 FIN 包时服务器的时间戳,peer->tcp_ts 记录的是 FIN 包中的时间戳,req->ts_recent 表示当前 SYN 包中的时间戳,因此 (u32)get_seconds() - peer->tcp_ts_stamp < TCP_PAWS_MSL 表示缓存还未过期,(s32)(peer->tcp_ts - req->ts_recent) > TCP_PAWS_WINDOW 表示当前 SYN 包中的时间戳比之前 FIN 包中的时间戳还小 1s, 因此认为当前 SYN 包时之前重传的,直接丢弃。

req - struct request_sock
peer- struct inet_peer, inet_getpeer()
get_seconds() - 获取当前服务器时间

TCP_PAWS_MSL

#define TCP_PAWS_MSL    60              /* Per-host timestamps are invalidated
                                         * after this time. It should be equal
                                         * (or greater than) TCP_TIMEWAIT_LEN
                                         * to provide reliability equal to one
                                         * provided by timewait state.

TCP_PAWS_WINDOW

#define TCP_PAWS_WINDOW 1               /* Replay window for per-host
                                         * timestamps. It must be less than
                                         * minimal timewait lifetime.
                                         */

tcp_v4_tw_remember_stamp()

int tcp_v4_tw_remember_stamp(struct inet_timewait_sock *tw)
{
        struct inet_peer *peer = inet_getpeer(tw->tw_daddr, 1);

        if (peer) {
                const struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);

                if ((s32)(peer->tcp_ts - tcptw->tw_ts_recent) <= 0 ||
                    ((u32)get_seconds() - peer->tcp_ts_stamp > TCP_PAWS_MSL &&
                     peer->tcp_ts_stamp <= (u32)tcptw->tw_ts_recent_stamp)) {
                        peer->tcp_ts_stamp = (u32)tcptw->tw_ts_recent_stamp;
                        peer->tcp_ts       = tcptw->tw_ts_recent;
                }
                inet_putpeer(peer);
                return 1;
        }

        return 0;
}

peer->tcp_ts_stamptcptw->tw_ts_recent_stamp, peer->tcp_tstcptw->tw_ts_recent, 见 tcp_timewait_state_process().

tcptw - struct tcp_timewait_sock, struct inet_timewait_sock

tcp_timewait_state_process()

/* FIN arrived, enter true time-wait state. */
tw->tw_substate   = TCP_TIME_WAIT;
tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
if (tmp_opt.saw_tstamp) {
        tcptw->tw_ts_recent_stamp = get_seconds();
        tcptw->tw_ts_recent       = tmp_opt.rcv_tsval;
}

/* I am shamed, but failed to make it more elegant.
 * Yes, it is direct reference to IP, which is impossible
 * to generalize to IPv6. Taking into account that IPv6
 * do not understand recycling in any case, it not
 * a big problem in practice. --ANK */
if (tw->tw_family == AF_INET &&
    tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp &&
    tcp_v4_tw_remember_stamp(tw))
        inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
                           TCP_TIMEWAIT_LEN);
else
        inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
                           TCP_TIMEWAIT_LEN);
return TCP_TW_ACK;

从此处可以看出,tcptw->tw_ts_recent_stamp 记录的是收到 FIN 包时服务器当前时间,而 tcptw->tw_ts_recent 记录的是 FIN 包中对端的时间戳。

结论

由于大量内侧玩家位于同一 NAT 网络中,所有玩家的 IP 被转换成同一出口 IP, 但时间戳却保留,因此到达服务器的包中时间戳顺序跟到达顺序是不一致的,当同时开启 tcp_tw_recycletcp_timestamps 时,一个玩家断开连接可能导致其他玩家的 SYN 包被丢弃掉。

实践

如何配置 tcp_tw_recycle 和 tcp_timestamps

tcp_timestamp 建议开启。tcp_timestamp 是 RFC1323 定义的优化选项,主要用于 TCP 连接中 RTT(Round Trip Time) 的计算,开启 tcp_timestamp 有利于系统计算更加准确的 RTT,也就有利于 TCP 性能的提升。

tcp_tw_recycle 建议关闭。

多数系统的默认设置也是如此。

为什么很多时候开启了 tcp_tw_recycle 也并没有产生问题

一是现网中很少出现批量用户处于同一 NAT 网络中且包被 NAT 网关转发之后顺序错乱,即使出现数量也很少;
二是有些客户端系统并没有使用时间戳,例如 XP 和多数 Windows 7.

你可能感兴趣的:(系统)