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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章