1. 程式人生 > 其它 >一次對server服務大量積壓異常TCP ESTABLISHED連結的排查筆記

一次對server服務大量積壓異常TCP ESTABLISHED連結的排查筆記

背景

我們都知道,基於Kubernetes的微服務,大行其道,傳統部署模式一直都在跟著變化,但其實,在原有業務向服務化方向過度過程中,有些場景可能會變得複雜。

比如說:將Kubernetes的模式應用到開發環節上,這個環節需要頻繁的變更程式碼,微服務的方式,可能就需要不斷的:

改程式碼->構建映象->映象推送->部署->拉去映象->生成容器

尤其是PHP的業務,不需要構建二進位制,僅需要釋出程式碼,因此,如果按照上面的部署方式,就需要頻繁改程式碼,走構建映象這個流程,最後再做釋出,這在開發環節就顯得過於麻煩了,換而言之,有沒有辦法,能讓開發直接將程式碼上傳到容器中呢?

其實是有的,就是設計一個FTP中介軟體代理,讓使用者本地改完程式碼,通過FTP客戶端(很多IDE是支援FTP的)直接上傳到容器內部,甚至於使用者儲存一下程式碼就上傳到容器內。

因此,這就引出了今天的主角,是我基於FTP協議+gRPC協議自研的FTP代理工具。

這個工具上線後,服務全公司所有研發,經過一段時間執行和修補,相對穩定,也做了一些關於記憶體方面的優化,直到又一次,在維護這個FTP代理的時候,發現一個奇怪的問題:

FTP代理程序,監聽的是 192.168.88.32 的 21 埠,所以,這個埠對應了多少連線,就表示有多少個客戶端存在,通過:

 

netstat -apn |grep "192.168.88.32:21"

發現,有將近1000個連結,且都是 ESTABLISHED,ESTABLISHED 狀態表示一個連線的狀態是“已連線”,但我們研發團隊,並沒有那麼多人,直覺上看,事出反常必有妖。

初步分析可能性

感覺可能有一種情況,就是每個人開了多個FTP客戶端,實際場景下,研發同學組可能會使用3種類型的FTP客戶端

PHPStorm:這個客戶端(SFTP外掛)自己會維護一個FTP長連線。
Sublime + VsCode,這2個客戶端不會維護連結,資料互動完成(比如傳輸任務),就主動傳送 QUIT 指令到FTP代理端,然後所有連結關閉。很乾淨。

另外,使用PHPStorm的話,也存在開多個IDE建立,就使用多個FTP客戶端連線的情況。
為了繼續排查,我把所有對 192.168.88.32:21 的連結,做了分組統計,看看哪個IP的連線數最多

 

  1.   # 注:61604 是 ftp代理的程序ID
  2.   netstat -apn|grep "61604/server"|grep '192.168.88.32:21'|awk -F ':' '{print$2}'|awk '{print$2}'|sort|uniq -c |sort

上面的統計,是看哪個IP,對 192.168.88.32:21 連線數最多(18個)。

統計發現,很多IP,都存在多個連結的情況,難道每個人都用了多個IDE且可能還多IDE視窗使用嗎?於是,挑了一個最多的,找到公司中使用這個IP的人,溝通發現,他確實使用了IDE多視窗,但是遠遠沒有使用18個客戶端那麼多,僅僅PHPStorm開了3個視窗而已。

初步排查結論:應該是FTP代理所在伺服器的問題,和使用者開多個客戶端沒有關係。

進一步排查

這次排查,是懷疑,這將近1000個的 ESTABLISHED 客戶端連結中,有大量假的 ESTABLISHED 連結存在,之前的統計發現,實際上,對 192.168.88.32:21 的客戶端連結進行篩選,得到的IP,一共才200個客戶端IP而已,平均下來,每個人都有5個FTP客戶端連結FTP代理,想象覺得不太可能。那麼,如何排查 ESTABLISHED 假連結呢?

在 TCP 四次揮手過程中,首先需要有一端,發起 FIN 包,接收方接受到 FIN 包之後,便開啟四次揮手的過程,這也是連線斷開的過程。

從之前的排檢視,有人的IP,發起了多達18個FTP連線,那麼,要排查是不是在 FTP 代理伺服器上,存在假的 ESTABLISHED 連線的話,就首先需要去 開發同學的機器上看,客戶端連線的埠,是不是仍在使用。比如:

 

tcp    ESTAB      0      0      192.168.88.32:21                 192.168.67.38:58038

這個表明,有一個研發的同學 IP是 192.168.67.38,使用了埠 58038,連線 192.168.88.32 上的 FTP 代理服務的 21 埠。所以,先要去看,到底研發同學的電腦上,這個埠存在不存在。

後來經過與研發同學溝通確認,研發電腦上並沒有 58038 埠使用,這說明,對FTP代理服務的的客戶端連結中顯示的埠,也就是實際使用者的客戶端埠,存在大量不存在的情況。

結論:FTP代理伺服器上,存在的近1000個客戶端連線中(ESTABLISHED狀態),有大量的假連線存在。也就是說,實際上這個連線早就斷開不存在了,但服務端卻還顯示存在。

排查假 ESTABLISHED 連線

首先,如果出現假的 ESTABLISHED 連線,表示連線的客戶端已經不存在了,客戶端一方,要麼發起了 TCP FIN 請求服務端沒有收到,比如因為網路的各種原因(比如斷網了)之後,FTP客戶端無法傳送FIN到服務端。要麼服務端伺服器接受到了 FIN,但是在後續過程中,丟包了等等。

為了驗證上面的問題,我本機進行了一次模擬,連線FTP服務端後,本機直接斷網,斷網後,殺死FTP客戶端程序,等待5分鐘(為什麼等待5分鐘後面說)後,重新聯網。然後再 FTP 服務端,檢視伺服器上與 FTP代理進行連線的所有IP,然後發現我本機的IP和埠依然在列,然後再我本機,通過

 

lsof -i :埠號

卻沒有任何記錄,直接說明:服務端確實保持了假 ESTABLISHED 連結,一直不釋放。

上面提到,我等待5分鐘,是因為,服務端的 keepalive,是這樣的配置:

 

  1.   [root@xx xx]# sysctl -a |grep keepalive
  2.   net.ipv4.tcp_keepalive_intvl = 75
  3.   net.ipv4.tcp_keepalive_probes = 9
  4.   net.ipv4.tcp_keepalive_time = 300

伺服器預設設定的 tcp keepalive 檢測是300秒後進行檢測,也就是5分鐘,當檢測失敗後,一共進行9次重試,每次時間間隔是75秒。
那麼,問題就來了,伺服器設定了 keepalive,如果 300 + 9*75 秒後,依然連線不上,就應該主動關閉假 ESTABLISHED 連線才對。為何還會積壓呢?

猜想1:大量的積壓的 ESTABLISHED 連線,實際上都還沒有到釋放時間

為了驗證這個問題,我們就需要具體的看某個連線,什麼時候建立的。所以,我找到其中一個我確定是假的 ESTABLISHED的連結(那個IP的使用者,把所有FTP客戶端都關了,程序也殺死了),看此連線的建立時間,過程如下:

先確定 FTP 代理程序的ID,為 61604

然後,看看這個程序的所有連線,找到某個埠的(55360,就是一個客戶端所使用的埠)

 

  1.   [root@xxx xxx]# lsof -p 61604|grep 55360
  2.   server 61604 root 6u IPv4 336087732 0t0 TCP node088032:ftp->192.168.70.16:55360 (ESTABLISHED)

我們看到一個 “6u”,這個就是程序使用的這個連線的socket檔案,Linux中,一切皆檔案。我們看看這個檔案的建立時間,就是這個連線的建立時間了

 

  1.   ll /proc/61604/fd/6
  2.   //輸出:
  3.   lrwx------. 1 root root 64 Nov 1 14:03 /proc/61604/fd/6 -> socket:[336087732]

這個連線是11月1號建立的,現在已經11月8號,這個時間,早已經超出了 keepalive 探測 TCP連線是否存活的時間。這說明2個點:

1、可能 Linux 的 KeepAlive 壓根沒生效。
2、可能我的 FTP 代理程序,壓根沒有使用 TCP KeepAlive

猜想2: FTP 代理程序,壓根沒有使用 TCP KeepAlive

要驗證這個結論,就得先知道,怎麼看一個連線,到底具不具備 KeepAlive 功效?

netstat 命令不好使(也可能我沒找到方法),我們使用 ss 命令,檢視 FTP程序下所有連線21埠的連結

 

ss -aoen|grep 192.168.12.32:21|grep ESTAB

從眾多結果中,隨便篩選2個結果:

 

  1.   tcp ESTAB 0 0 192.168.12.32:21 192.168.20.63:63677 ino:336879672 sk:65bb <->
  2.   tcp ESTAB 0 0 192.168.12.32:21 192.168.49.21:51896 ino:336960511 sk:67f7 <->

我們再對比一下,所有連線伺服器sshd程序的

 

  1.   tcp ESTAB 0 0 192.168.12.32:333 192.168.53.207:63269 timer:(keepalive,59sec,0) ino:336462258 sk:6435 <->
  2.   tcp ESTAB 0 0 192.168.12.32:333 192.168.55.185:64892 timer:(keepalive,3min59sec,0) ino:336461969 sk:62d1 <->
  3.   tcp ESTAB 0 0 192.168.12.32:333 192.168.53.207:63220 timer:(keepalive,28sec,0) ino:336486442 sk:6329 <->
  4.   tcp ESTAB 0 0 192.168.12.32:333 192.168.53.207:63771 timer:(keepalive,12sec,0) ino:336896561 sk:65de <->

對比很容易發現,連線 21埠的所有連線,多沒有 timer 項。這說明,FTP代理 程序監聽 21 埠時,所有進來的連結,全都沒有使用keepalive。

找了一些文章,大多隻是說,怎麼配置Linux 的 Keep Alive,以及不配置的,會造成 ESTABLISHED 不釋放問題,沒有說程序需要額外設定啊?難道 Linux KeepAlive 配置,不是對所有連線直接就生效的?

所以,我們有必要驗證 Linux keepalive,必須要程序自己額外開啟才能生效

驗證 Linux keepalive,必須要程序自己額外開啟才能生效

在開始這個驗證之前,先摘取一段FTP中介軟體代理關於監聽 21 埠的部分程式碼:

 

  1.   func (ftpServer *FTPServer) ListenAndServe() error {
  2.   laddr, err := net.ResolveTCPAddr("tcp4", ftpServer.listenTo)
  3.   if err != nil {
  4.   return err
  5.   }
  6.   listener, err := net.ListenTCP("tcp4", laddr)
  7.   if err != nil {
  8.   return err
  9.   }
  10.   for {
  11.   clientConn, err := listener.AcceptTCP()
  12.   if err != nil || clientConn == nil {
  13.   ftpServer.logger.Print("listening error")
  14.   break
  15.   }
  16.   //以閉包的方式整理處理driver和ftpBridge,協程結束整體由GC做資源釋放
  17.   go func(c *net.TCPConn) {
  18.   driver, err := ftpServer.driverFactory.NewDriver(ftpServer.FTPDriverType)
  19.   if err != nil {
  20.   ftpServer.logger.Print("Error creating driver, aborting client connection:" + err.Error())
  21.   } else {
  22.   ftpBridge := NewftpBridge(c, driver)
  23.   ftpBridge.Serve()
  24.   }
  25.   c = nil
  26.   }(clientConn)
  27.   }
  28.   return nil
  29.   }

足夠明顯,整個函式,net.ListenTCP 附近都沒有任何設定KeepAlive的相關操作。我們檢視 相關函式,找到了設定 KeepAlive的地方,進行一下設定:

 

  1.   if err != nil || clientConn == nil {
  2.   ftpServer.logger.Print("listening error")
  3.   break
  4.   }
  5.   // 此處,設定 keepalive
  6.   clientConn.SetKeepAlive(true)

重新構建部署之後,可以看到,所有對21埠的連線,全部都帶了 timer

 

ss -aoen|grep 192.168.12.32:21|grep ESTAB

輸出如下:

 

  1.   tcp ESTAB 0 0 192.168.12.32:21 192.168.70.76:54888 timer:(keepalive,1min19sec,0) ino:397279721 sk:6b49 <->
  2.   tcp ESTAB 0 0 192.168.12.32:21 192.168.37.125:49648 timer:(keepalive,1min11sec,0) ino:398533882 sk:6b4a <->
  3.   tcp ESTAB 0 0 192.168.12.32:21 192.168.33.196:64471 timer:(keepalive,7.957ms,0) ino:397757143 sk:6b4c <->
  4.   tcp ESTAB 0 0 192.168.12.32:21 192.168.21.159:56630 timer:(keepalive,36sec,0) ino:396741646 sk:6b4d <->

可以很明顯看到,所有的連線,全部具備了 timer 功效,說明:想要使用 Linux 的 KeepAlive,需要程式單獨做設定進行開啟才行。

最後:ss 命令結果中 keepalive 的說明

首先,看一下 Linux 中的配置,我的機器如下:

 

  1.   [root@xx xx]# sysctl -a |grep keepalive
  2.   net.ipv4.tcp_keepalive_intvl = 75
  3.   net.ipv4.tcp_keepalive_probes = 9
  4.   net.ipv4.tcp_keepalive_time = 300

tcp_keepalive_time:表示多長時間後,開始檢測TCP連結是否有效。
tcp_keepalive_probes:表示如果檢測失敗,會一直探測 9 次。
tcp_keepalive_intvl:承上,探測9次的時間間隔為 75 秒。

然後,我們看一下 ss 命令的結果:

 

ss -aoen|grep 192.168.12.32:21|grep ESTAB

 

tcp ESTAB  0  0  192.168.12.32:21 192.168.70.76:54888 timer:(keepalive,1min19sec,0) ino:397279721 sk:6b49 <->

摘取這部分:timer:(keepalive,1min19sec,0) ,其中:

keepalive:表示此連結具備 keepalive 功效。
1min19sec:表示剩餘探測時間,這個時間每次看都會邊,是一個遞減的值,第一次探測,需要 net.ipv4.tcp_keepalive_time 這個時間倒計時,如果探測失敗繼續探測,後邊會按照 net.ipv4.tcp_keepalive_intvl 這個時間值進行探測。直到探測成功。
0:這個值是探測時,檢測到這是一個無效的TCP連結的話已經進行了的探測次數。