TCP漫谈之keepalive和time_wait

简介:

TCP漫谈之keepalive和time_wait

摘要:介绍TCP keepalive和TCP time_wait。

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

一、TCP keepalive

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

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

建立连接后,客户端或者服务端便可以通过已建立的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,是因为发现服器上有泄露的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。

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

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

# tcpdump -i bond0 port 35832 -nvv -A

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

二、TCP Time_wait

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

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

有两个原因:

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

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

为此设计了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。

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

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

作者:陈晓宇

作者著作《云计算那些事儿:从IaaS到PaaS进阶》

相关实践学习
深入解析Docker容器化技术
Docker是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化,容器是完全使用沙箱机制,相互之间不会有任何接口。Docker是世界领先的软件容器平台。开发人员利用Docker可以消除协作编码时“在我的机器上可正常工作”的问题。运维人员利用Docker可以在隔离容器中并行运行和管理应用,获得更好的计算密度。企业利用Docker可以构建敏捷的软件交付管道,以更快的速度、更高的安全性和可靠的信誉为Linux和Windows Server应用发布新功能。 在本套课程中,我们将全面的讲解Docker技术栈,从环境安装到容器、镜像操作以及生产环境如何部署开发的微服务应用。本课程由黑马程序员提供。 &nbsp; &nbsp; 相关的阿里云产品:容器服务 ACK 容器服务 Kubernetes 版(简称 ACK)提供高性能可伸缩的容器应用管理能力,支持企业级容器化应用的全生命周期管理。整合阿里云虚拟化、存储、网络和安全能力,打造云端最佳容器化应用运行环境。 了解产品详情: https://www.aliyun.com/product/kubernetes
相关文章
|
网络协议 Linux 关系型数据库
TCP Keepalive HOWTO
TCP Keepalive HOWTO Fabio Busatto 2007-05-04 Revision History Revision 1.0 2007-05-04 Revised by: FB First release, reviewed by TM.
1340 0
|
网络协议 Linux PHP
TCP TIME_WAIT状态解析及问题解决
版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaobryant/article/details/80557158 一、TCP四次挥手过程 TCP在建立连接时需要握手,同理,在关闭连接的时候也需要握手。
2843 0
|
网络协议
TCP连接和 time_wait、close_waite
TCP连接和 time_wait、close_waite tags:time_wait close_waite RST TCP 引言:前两天朋友公司的服务器垮掉了,最后查出的原因是发现大量的time_wait网络状态。
1739 0
|
网络协议 Unix 网络安全