背景
我們都知道,基于Kubernetes的微服務,大行其道,傳統(tǒng)部署模式一直都在跟著變化,但其實,在原有業(yè)務向服務化方向過度過程中,有些場景可能會變得復雜。
比如說:將Kubernetes的模式應用到開發(fā)環(huán)節(jié)上,這個環(huán)節(jié)需要頻繁的變更代碼,微服務的方式,可能就需要不斷的:
改代碼->構(gòu)建鏡像->鏡像推送->部署->拉去鏡像->生成容器
尤其是PHP的業(yè)務,不需要構(gòu)建二進制,僅需要發(fā)布代碼,因此,如果按照上面的部署方式,就需要頻繁改代碼,走構(gòu)建鏡像這個流程,最后再做發(fā)布,這在開發(fā)環(huán)節(jié)就顯得過于麻煩了,換而言之,有沒有辦法,能讓開發(fā)直接將代碼上傳到容器中呢?
其實是有的,就是設(shè)計一個FTP中間件代理,讓用戶本地改完代碼,通過FTP客戶端(很多IDE是支持FTP的)直接上傳到容器內(nèi)部,甚至于用戶保存一下代碼就上傳到容器內(nèi)。
因此,這就引出了今天的主角,是我基于FTP協(xié)議+gRPC協(xié)議自研的FTP代理工具。
這個工具上線后,服務全公司所有研發(fā),經(jīng)過一段時間運行和修補,相對穩(wěn)定,也做了一些關(guān)于內(nèi)存方面的優(yōu)化,直到又一次,在維護這個FTP代理的時候,發(fā)現(xiàn)一個奇怪的問題:
FTP代理進程,監(jiān)聽的是 192.168.88.32 的 21 端口,所以,這個端口對應了多少連接,就表示有多少個客戶端存在,通過:
netstat -apn |grep "192.168.88.32:21"
發(fā)現(xiàn),有將近1000個鏈接,且都是 ESTABLISHED,ESTABLISHED 狀態(tài)表示一個連接的狀態(tài)是“已連接”,但我們研發(fā)團隊,并沒有那么多人,直覺上看,事出反常必有妖。
初步分析可能性
感覺可能有一種情況,就是每個人開了多個FTP客戶端,實際場景下,研發(fā)同學組可能會使用3種類型的FTP客戶端
PHPStorm:這個客戶端(SFTP插件)自己會維護一個FTP長連接。
Sublime + VsCode,這2個客戶端不會維護鏈接,數(shù)據(jù)交互完成(比如傳輸任務),就主動發(fā)送 QUIT 指令到FTP代理端,然后所有鏈接關(guān)閉。很干凈。
另外,使用PHPStorm的話,也存在開多個IDE創(chuàng)建,就使用多個FTP客戶端連接的情況。
為了繼續(xù)排查,我把所有對 192.168.88.32:21 的鏈接,做了分組統(tǒng)計,看看哪個IP的連接數(shù)最多
# 注: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
上面的統(tǒng)計,是看哪個IP,對 192.168.88.32:21 連接數(shù)最多(18個)。
統(tǒng)計發(fā)現(xiàn),很多IP,都存在多個鏈接的情況,難道每個人都用了多個IDE且可能還多IDE窗口使用嗎?于是,挑了一個最多的,找到公司中使用這個IP的人,溝通發(fā)現(xiàn),他確實使用了IDE多窗口,但是遠遠沒有使用18個客戶端那么多,僅僅PHPStorm開了3個窗口而已。
初步排查結(jié)論:應該是FTP代理所在服務器的問題,和用戶開多個客戶端沒有關(guān)系。
進一步排查
這次排查,是懷疑,這將近1000個的 ESTABLISHED 客戶端鏈接中,有大量假的 ESTABLISHED 鏈接存在,之前的統(tǒng)計發(fā)現(xiàn),實際上,對 192.168.88.32:21 的客戶端鏈接進行篩選,得到的IP,一共才200個客戶端IP而已,平均下來,每個人都有5個FTP客戶端鏈接FTP代理,想象覺得不太可能。那么,如何排查 ESTABLISHED 假鏈接呢?
在 TCP 四次揮手過程中,首先需要有一端,發(fā)起 FIN 包,接收方接受到 FIN 包之后,便開啟四次揮手的過程,這也是連接斷開的過程。
從之前的排查看,有人的IP,發(fā)起了多達18個FTP連接,那么,要排查是不是在 FTP 代理服務器上,存在假的 ESTABLISHED 連接的話,就首先需要去 開發(fā)同學的機器上看,客戶端連接的端口,是不是仍在使用。比如:
tcp ESTAB 0 0 192.168.88.32:21 192.168.67.38:58038
這個表明,有一個研發(fā)的同學 IP是 192.168.67.38,使用了端口 58038,連接 192.168.88.32 上的 FTP 代理服務的 21 端口。所以,先要去看,到底研發(fā)同學的電腦上,這個端口存在不存在。
后來經(jīng)過與研發(fā)同學溝通確認,研發(fā)電腦上并沒有 58038 端口使用,這說明,對FTP代理服務的的客戶端鏈接中顯示的端口,也就是實際用戶的客戶端端口,存在大量不存在的情況。
結(jié)論:FTP代理服務器上,存在的近1000個客戶端連接中(ESTABLISHED狀態(tài)),有大量的假連接存在。也就是說,實際上這個連接早就斷開不存在了,但服務端卻還顯示存在。
排查假 ESTABLISHED 連接
首先,如果出現(xiàn)假的 ESTABLISHED 連接,表示連接的客戶端已經(jīng)不存在了,客戶端一方,要么發(fā)起了 TCP FIN 請求服務端沒有收到,比如因為網(wǎng)絡的各種原因(比如斷網(wǎng)了)之后,F(xiàn)TP客戶端無法發(fā)送FIN到服務端。要么服務端服務器接受到了 FIN,但是在后續(xù)過程中,丟包了等等。
為了驗證上面的問題,我本機進行了一次模擬,連接FTP服務端后,本機直接斷網(wǎng),斷網(wǎng)后,殺死FTP客戶端進程,等待5分鐘(為什么等待5分鐘后面說)后,重新聯(lián)網(wǎng)。然后再 FTP 服務端,查看服務器上與 FTP代理進行連接的所有IP,然后發(fā)現(xiàn)我本機的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
服務器默認設(shè)置的 tcp keepalive 檢測是300秒后進行檢測,也就是5分鐘,當檢測失敗后,一共進行9次重試,每次時間間隔是75秒。
那么,問題就來了,服務器設(shè)置了 keepalive,如果 300 + 9*75 秒后,依然連接不上,就應該主動關(guān)閉假 ESTABLISHED 連接才對。為何還會積壓呢?
猜想1:大量的積壓的 ESTABLISHED 連接,實際上都還沒有到釋放時間
為了驗證這個問題,我們就需要具體的看某個連接,什么時候創(chuàng)建的。所以,我找到其中一個我確定是假的 ESTABLISHED的鏈接(那個IP的用戶,把所有FTP客戶端都關(guān)了,進程也殺死了),看此連接的創(chuàng)建時間,過程如下:
先確定 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中,一切皆文件。我們看看這個文件的創(chuàng)建時間,就是這個連接的創(chuàng)建時間了
ll /proc/61604/fd/6
//輸出:
lrwx------. 1 root root 64 Nov 1 14:03 /proc/61604/fd/6 -> socket:[336087732]
這個連接是11月1號創(chuàng)建的,現(xiàn)在已經(jīng)11月8號,這個時間,早已經(jīng)超出了 keepalive 探測 TCP連接是否存活的時間。這說明2個點:
1、可能 Linux 的 KeepAlive 壓根沒生效。
2、可能我的 FTP 代理進程,壓根沒有使用 TCP KeepAlive
猜想2: FTP 代理進程,壓根沒有使用 TCP KeepAlive
要驗證這個結(jié)論,就得先知道,怎么看一個連接,到底具不具備 KeepAlive 功效?
netstat 命令不好使(也可能我沒找到方法),我們使用 ss 命令,查看 FTP進程下所有連接21端口的鏈接
ss -aoen|grep 192.168.12.32:21|grep ESTAB
從眾多結(jié)果中,隨便篩選2個結(jié)果:
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 <->
對比很容易發(fā)現(xiàn),連接 21端口的所有連接,多沒有 timer 項。這說明,F(xiàn)TP代理 進程監(jiān)聽 21 端口時,所有進來的鏈接,全都沒有使用keepalive。
找了一些文章,大多只是說,怎么配置Linux 的 Keep Alive,以及不配置的,會造成 ESTABLISHED 不釋放問題,沒有說進程需要額外設(shè)置?。侩y道 Linux KeepAlive 配置,不是對所有連接直接就生效的?
所以,我們有必要驗證 Linux keepalive,必須要進程自己額外開啟才能生效
驗證 Linux keepalive,必須要進程自己額外開啟才能生效
在開始這個驗證之前,先摘取一段FTP中間件代理關(guān)于監(jiān)聽 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,協(xié)程結(jié)束整體由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
}
足夠明顯,整個函數(shù),net.ListenTCP 附近都沒有任何設(shè)置KeepAlive的相關(guān)操作。我們查看 相關(guān)函數(shù),找到了設(shè)置 KeepAlive的地方,進行一下設(shè)置:
if err != nil || clientConn == nil {
ftpServer.logger.Print("listening error")
break
}
// 此處,設(shè)置 keepalive
clientConn.SetKeepAlive(true)
重新構(gòu)建部署之后,可以看到,所有對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,需要程序單獨做設(shè)置進行開啟才行。
最后:ss 命令結(jié)果中 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 命令的結(jié)果:
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 這個時間倒計時,如果探測失敗繼續(xù)探測,后邊會按照 net.ipv4.tcp_keepalive_intvl 這個時間值進行探測。直到探測成功。
0:這個值是探測時,檢測到這是一個無效的TCP鏈接的話已經(jīng)進行了的探測次數(shù)。
歡迎關(guān)注“海角之南”公眾號獲取更新動態(tài)
