TCP keepalive time_wait

TCP Keepalive与Time_wait状态详解:优化连接管理和迷路数据包
本文探讨了TCP协议中的Keepalive机制如何检测连接状态,以及Time_wait状态的设计目的和优化策略。重点讲解了为何需要Time_wait状态,如何避免迷路数据包和解决连接遗留问题,以及Linux参数对这些行为的影响。

以下内容转载自 https://www.toutiao.com/i6813908431703900683/

IT知识课堂2020-04-10 16:20:00

TCP是一个有状态通讯协议,所谓的有状态是指通信过程中通信的双方各自维护连接的状态。

一、TCP keepalive

先简单回顾一下TCP连接建立和断开的整个过程。(这里主要考虑主流程,关于丢包、拥塞、窗口、失败重试等情况后面详细讨论。)

首先是客户端发送syn(Synchronize Sequence Numbers:同步序列编号)包给服务端,告诉服务端我要连接你,syn包里面主要携带了客户端的seq序列号;服务端回发一个syn+ack,其中syn包和客户端原理类似,只不过携带的是服务端的seq序列号,ack包则是确认客户端允许连接;最后客户端再次发送一个ack确认接收到服务端的syn包。这样客户端和服务端就可以建立连接了。整个流程称为三次握手。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

建立连接后,客户端或者服务端便可以通过已建立的socket连接发送数据,对端接收数据后,便可以通过ack确认已经收到数据。

数据交换完毕后,通常是客户端便可以发送FIN包,告诉另一端我要断开了;另一端先通过ack确认收到FIN包,然后发送FIN包告诉客户端我也关闭了;最后客户端回应ack确认连接终止。整个流程成为四次挥手。

TCP的性能经常为大家所诟病,除了TCP+IP额外的header以外,它建立连接需要三次握手,关闭连接需要四次挥手。如果只是发送很少的数据,那么传输的有效数据是非常少的。

是不是建立一次连接后续可以继续复用呢?的确可以这样做,但这又带来另一个问题,如果连接一直不释放,端口被占满了咋办。为此引入了今天讨论的第一个话题TCP keepalive。所谓的TCP keepalive是指TCP连接建立后会通过keepalive的方式一直保持,不会在数据传输完成后立刻中断,而是通过keepalive机制检测连接状态。

Linux控制keepalive有三个参数:保活时间net.ipv4.tcp_keepalive_time、保活时间间隔net.ipv4.tcp_keepalive_intvl、保活探测次数net.ipv4.tcp_keepalive_probes,默认值分别是 7200 秒(2 小时)、75 秒和 9 次探测。如果使用 TCP 自身的 keep-Alive 机制,在 Linux 系统中,最少需要经过 2 小时 + 9*75 秒后断开。譬如我们SSH登录一台服务器后可以看到这个TCP的keepalive时间是2个小时,并且会在2个小时后发送探测包,确认对端是否处于连接状态。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

之所以会讨论TCP的keepalive,是因为发现服器上有泄露的TCP连接:

# ll /proc/11516/fd/10
lrwx------ 1 root root 64 Jan  3 19:04 /proc/11516/fd/10 -> socket:[1241854730]
# date
Sun Jan  5 17:39:51 CST 2020

已经建立连接两天,但是对方已经断开了(非正常断开)。由于使用了比较老的go(1.9之前版本有问题)导致连接没有释放。

解决这类问题,可以借助TCP的keepalive机制。新版go语言支持在建立连接的时候设置keepalive时间。首先查看网络包中建立TCP连接的DialContext方法中

if tc, ok := c.(*TCPConn); ok && d.KeepAlive >= 0 {
   setKeepAlive(tc.fd, true)
   ka := d.KeepAlive
   if d.KeepAlive == 0 {
      ka = defaultTCPKeepAlive
   }
   setKeepAlivePeriod(tc.fd, ka)
   testHookSetKeepAlive(ka)
}

其中defaultTCPKeepAlive是15s。如果是HTTP连接,使用默认client,那么它会将keepalive时间设置成30s。

var DefaultTransport RoundTripper = &Transport{
   Proxy: ProxyFromEnvironment,
   DialContext: (&net.Dialer{
      Timeout:   30 * time.Second,
      KeepAlive: 30 * time.Second,
      DualStack: true,
   }).DialContext,
   ForceAttemptHTTP2:     true,
   MaxIdleConns:          100,
   IdleConnTimeout:       90 * time.Second,
   TLSHandshakeTimeout:   10 * time.Second,
   ExpectContinueTimeout: 1 * time.Second,
}

下面通过一个简单的demo测试一下,代码如下:

func main() {

   wg := &sync.WaitGroup{}

   c := http.DefaultClient
   for i := 0; i < 2; i++ {
      wg.Add(1)
      go func() {
         defer wg.Done()
         for {
            r, err := c.Get("http://10.143.135.95:8080")
            if err != nil {
               fmt.Println(err)
               return
            }
            _, err = ioutil.ReadAll(r.Body)
            r.Body.Close()
            if err != nil {
               fmt.Println(err)
               return
            }

            time.Sleep(30 * time.Millisecond)
         }
      }()
   }
   wg.Wait()
}

执行程序后,可以查看连接。初始设置keepalive为30s。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

然后不断递减,至0后,又会重新获取30s。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

整个过程可以通过tcpdump抓包获取。

# tcpdump -i bond0 port 35832 -nvv -A

其实很多应用并非是通过TCP的keepalive机制探活的,因为默认的两个多小时检查时间对于很多实时系统是完全没法满足的,通常的做法是通过应用层的定时监测,如PING-PONG机制(就像打乒乓球,一来一回),应用层每隔一段时间发送心跳包,如websocket的ping-pong。

二、TCP Time_wait

第二个希望和大家分享的话题是TCP的Time_wait状态。、

深入分析TCP的keepalive和time_wait,总能发现新东西

 

为啥需要time_wait状态呢?为啥不直接进入closed状态呢?直接进入closed状态能更快地释放资源给新的连接使用了,而不是还需要等待2MSL(Linux默认)时间。

有两个原因:

一是为了防止“迷路的数据包”,如下图所示,如果在第一个连接里第三个数据包由于底层网络故障延迟送达。等待新的连接建立后,这个迟到的数据包才到达,那么将会导致接收数据紊乱。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

第二个原因则更加简单,如果因为最后一个ack丢失,那么对方将一直处于last ack状态,如果此时重新发起新的连接,对方将返回RST包拒绝请求,将会导致无法建立新连接。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

为此设计了time_wait状态。在高并发情况下,如果能将time_wait的TCP复用, time_wait复用是指可以将处于time_wait状态的连接重复利用起来。从time_wait转化为established,继续复用。Linux内核通过net.ipv4.tcp_tw_reuse参数控制是否开启time_wait状态复用。

读者可能很好奇,之前不是说time_wait设计之初是为了解决上面两个问题的吗?如果直接复用不是反而会导致上面两个问题出现吗?这里先介绍Linux默认开启的一个TCP时间戳策略net.ipv4.tcp_timestamps = 1。

深入分析TCP的keepalive和time_wait,总能发现新东西

 

时间戳开启后,针对第一个迷路数据包的问题,由于晚到数据包的时间戳过早会被直接丢弃,不会导致新连接数据包紊乱;针对第二个问题,开启reuse后,当对方处于last-ack状态时,发送syn包会返回FIN,ACK包,然后客户端发送RST让服务端关闭请求,从而客户端可以再次发送syn建立新的连接。

最后还需要提醒读者的是,Linux 4.1内核版本之前除了tcp_tw_reuse以外,还有一个参数tcp_tw_recycle,这个参数就是强制回收time_wait状态的连接,它会导致NAT环境丢包,所以不建议开启。

 

<think>嗯,用户这次的问题是关于配置Linux系统的TCP参数来优化网络性能。用户明确给出了四个要添加的参数:tcp_max_tw_buckets、tcp_tw_reuse、tcp_keepalive_probes和tcp_keepalive_intvl。看来用户已经做过一些功课,对TCP调优有一定了解。 从用户列出的参数看,主要关注点在TIME_WAIT状态管理和TCP keepalive机制。tcp_max_tw_buckets控制TIME_WAIT连接的最大数量,tcp_tw_reuse则允许重用TIME_WAIT连接,这两个参数对解决高并发场景下的端口耗尽问题很关键。而调整keepalive相关参数说明用户可能遇到了长连接场景下的连接稳定性问题。 注意到用户把tcp_keepalive_intvl参数名误写成了tcp_keepalive_intv1,这可能是笔误。在回复时需要特别指出这点,避免配置错误。另外用户没有提到tcp_timestamps参数,而tcp_tw_reuse需要依赖时间戳功能,这点也需要提醒。 用户提供的三个引用资料很有参考价值。引用[1]提到tcp_tw_reuse需要结合tcp_timestamps使用;引用[2]强调了不同Linux版本的内核参数命名差异;引用[3]解释了backlog参数的作用。这些都需要整合到回复中。 考虑到用户正在编辑sysctl.conf文件,应该给出完整的配置示例,包括所有必要参数。除了用户提到的四个参数外,建议补充tcp_timestamps、tcp_fin_timeout等关联参数,并说明参数间的依赖关系。 用户可能是在运维高并发服务的场景,所以需要强调参数调整后的测试验证步骤。特别是要提醒tcp_tw_reuse在NAT环境下可能存在的问题,以及keepalive参数调整对应用层心跳机制的影响。 最后要给出具体的操作指引:如何修改文件、使配置生效的指令、验证方法等。考虑到用户使用vi编辑器,说明应该简洁准确。测试部分可以建议用ss命令查看TIME_WAIT状态变化,用sysctl命令检查参数加载情况。</think>以下是为优化Linux系统TCP参数在`/etc/sysctl.conf`中的配置建议及详细说明,结合您的需求和引用内容: --- ### **配置建议** 在`/etc/sysctl.conf`中添加以下参数: ```bash net.ipv4.tcp_max_tw_buckets = 10000 # 限制TIME_WAIT状态连接的最大数量 net.ipv4.tcp_tw_reuse = 1 # 允许重用TIME_WAIT状态的连接(需开启时间戳) net.ipv4.tcp_keepalive_probes = 9 # 发送TCP keepalive探测包的次数 net.ipv4.tcp_keepalive_intvl = 30 # 探测包发送间隔(秒)(注意参数名应为tcp_keepalive_intvl) ``` --- ### **参数解析与注意事项** 1. **`net.ipv4.tcp_max_tw_buckets`** - 限制系统中`TIME_WAIT`状态连接的最大数量(默认约18,000),超出后新连接会直接关闭。 - **作用**:防止`TIME_WAIT`连接耗尽内存或端口资源,尤其在高并发短连接场景[^1]。 2. **`net.ipv4.tcp_tw_reuse = 1`** - 允许内核重用处于`TIME_WAIT`状态的连接,**前提是开启时间戳**(`net.ipv4.tcp_timestamps=1`)。 - **优势**:显著提升短连接性能,减少端口占用[^1]。 - **风险**:在复杂NAT环境中可能引发兼容性问题,需测试验证[^1]。 3. **`net.ipv4.tcp_keepalive_*`** - `probes=9`:发送9次keepalive探测包(默认9次)后判定连接失效。 - `intvl=30`:探测包间隔30秒(默认75秒),**注意参数名应为`tcp_keepalive_intvl`而非`intv1`**。 - **场景**:适用于需要快速检测僵死长连接的场景(如数据库、负载均衡)。 --- ### **必须补充的关键参数** ```bash net.ipv4.tcp_timestamps = 1 # 启用TCP时间戳(tcp_tw_reuse的依赖条件) net.ipv4.tcp_fin_timeout = 30 # 缩短FIN_WAIT_2状态超时(默认60秒) net.ipv4.tcp_syncookies = 1 # 防御SYN洪水攻击 net.netfilter.nf_conntrack_max = 65536 # 调整连接跟踪表大小(CentOS 6+使用此参数名[^2]) ``` --- ### **操作步骤** 1. 编辑配置文件: ```bash sudo vi /etc/sysctl.conf ``` 2. 粘贴上述所有参数,保存退出。 3. 加载配置并验证: ```bash sudo sysctl -p # 立即生效 sysctl net.ipv4.tcp_tw_reuse # 检查参数值(应为1) ``` --- ### **性能影响与测试建议** - **`tcp_tw_reuse`**:通过`ss -s`观察`TIME_WAIT`数量变化,监控业务稳定性[^1]。 - **Keepalive参数**:调整后需确保应用层心跳机制不受干扰。 - **连接跟踪表**:若使用防火墙/NAT,需匹配`nf_conntrack_max`与业务连接数[^2]。 - **Backlog队列**:若涉及高并发请求,同步调整`net.core.somaxconn`(监听队列上限)[^3]。 --- ### **相关问题** 1. `tcp_tw_reuse`和`tcp_tw_recycle`有何区别?为何后者已被弃用? 2. 如何监控Linux系统的`TIME_WAIT`连接数量及变化趋势? 3. TCP keepalive机制与应用层心跳(如HTTP Keep-Alive)如何协同工作? 4. 调整`nf_conntrack_max`时,如何计算合理的连接跟踪表大小? > 引用说明: > [^1]: `tcp_tw_reuse`需依赖时间戳且需测试兼容性,尤其适合短连接场景。 > [^2]: CentOS 6+系统中连接跟踪参数名为`nf_conntrack_max`而非`ip_conntrack_max`。 > [^3]: `net.core.somaxconn`影响监听队列容量,队列满时新请求将被拒绝。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值