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

背景

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

好比說:將Kubernetes的模式應用到開發環節上,這個環節須要頻繁的變動代碼,微服務的方式,可能就須要不斷的:服務器

改代碼->構建鏡像->鏡像推送->部署->拉去鏡像->生成容器

尤爲是PHP的業務,不須要構建二進制,僅須要發佈代碼,所以,若是按照上面的部署方式,就須要頻繁改代碼,走構建鏡像這個流程,最後再作發佈,這在開發環節就顯得過於麻煩了,換而言之,有沒有辦法,能讓開發直接將代碼上傳到容器中呢? 網絡

實際上是有的,就是設計一個FTP中間件代理,讓用戶本地改完代碼,經過FTP客戶端(不少IDE是支持FTP的)直接上傳到容器內部,甚至於用戶保存一下代碼就上傳到容器內。 閉包

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

這個工具上線後,服務全公司全部研發,通過一段時間運行和修補,相對穩定,也作了一些關於內存方面的優化,直到又一次,在維護這個FTP代理的時候,發現一個奇怪的問題: socket

FTP代理進程,監聽的是 192.168.88.32 的 21 端口,因此,這個端口對應了多少鏈接,就表示有多少個客戶端存在,經過:tcp

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的鏈接數最多工具

# 注:61604 是 ftp代理的進程ID
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,是這樣的配置:

[root@xx xx]# sysctl -a |grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
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,就是一個客戶端所使用的端口)

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

咱們看到一個 「6u」,這個就是進程使用的這個鏈接的socket文件,Linux中,一切皆文件。咱們看看這個文件的建立時間,就是這個鏈接的建立時間了

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

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

一、可能 Linux 的 KeepAlive 壓根沒生效。
二、可能個人 FTP 代理進程,壓根沒有使用 TCP KeepAlive

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

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

netstat 命令很差使(也可能我沒找到方法),咱們使用 ss 命令,查看 FTP進程下全部鏈接21端口的連接

ss -aoen|grep 192.168.12.32:21|grep ESTAB

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

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

咱們再對比一下,全部鏈接服務器sshd進程的

tcp    ESTAB      0      0      192.168.12.32:333                192.168.53.207:63269               timer:(keepalive,59sec,0) ino:336462258 sk:6435 <->
tcp    ESTAB      0      0      192.168.12.32:333                192.168.55.185:64892               timer:(keepalive,3min59sec,0) ino:336461969 sk:62d1 <->
tcp    ESTAB      0      0      192.168.12.32:333                192.168.53.207:63220               timer:(keepalive,28sec,0) ino:336486442 sk:6329 <->
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 端口的部分代碼:

func (ftpServer *FTPServer) ListenAndServe() error {
    laddr, err := net.ResolveTCPAddr("tcp4", ftpServer.listenTo)
    if err != nil {
        return err
    }
    listener, err := net.ListenTCP("tcp4", laddr)
    if err != nil {
        return err
    }
    for {
        clientConn, err := listener.AcceptTCP()
        if err != nil || clientConn == nil {
            ftpServer.logger.Print("listening error")
            break
        }
        //以閉包的方式整理處理driver和ftpBridge,協程結束總體由GC作資源釋放
        go func(c *net.TCPConn) {
            driver, err := ftpServer.driverFactory.NewDriver(ftpServer.FTPDriverType)
            if err != nil {
                ftpServer.logger.Print("Error creating driver, aborting client connection:" + err.Error())
            } else {
                ftpBridge := NewftpBridge(c, driver)
                ftpBridge.Serve()
            }
            c = nil
        }(clientConn)
    }
    return nil
}

足夠明顯,整個函數,net.ListenTCP 附近都沒有任何設置KeepAlive的相關操做。咱們查看 相關函數,找到了設置 KeepAlive的地方,進行一下設置:

if err != nil || clientConn == nil {
    ftpServer.logger.Print("listening error")
    break
}
// 此處,設置 keepalive
clientConn.SetKeepAlive(true)

從新構建部署以後,能夠看到,全部對21端口的鏈接,所有都帶了 timer

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 <->
tcp    ESTAB      0      0      192.168.12.32:21                 192.168.37.125:49648              timer:(keepalive,1min11sec,0) ino:398533882 sk:6b4a <->
tcp    ESTAB      0      0      192.168.12.32:21                 192.168.33.196:64471              timer:(keepalive,7.957ms,0) ino:397757143 sk:6b4c <->
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 中的配置,個人機器以下:

[root@xx xx]# sysctl -a |grep keepalive
net.ipv4.tcp_keepalive_intvl = 75
net.ipv4.tcp_keepalive_probes = 9
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連接的話已經進行了的探測次數。

歡迎關注「海角之南」公衆號獲取更新動態

圖片描述

相關文章
相關標籤/搜索