TCP漫谈之keepalive

tcp是一个有状态通讯协议,所谓的有状态是指通信过程中通信的双方各自维护连接的状态。
先简单回顾一下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_keepalve_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://x.x.x.x: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。

发布了215 篇原创文章 · 获赞 103 · 访问量 461万+

猜你喜欢

转载自blog.csdn.net/u010278923/article/details/105201692
今日推荐