當心!TCP本機用戶端串連本機伺服器
上周,在我們進行效能測試的時候,發現了一個問題。
我們的伺服器上啟了一個redis服務端,偵聽0.0.0.0的1234連接埠,同處在原生另外一個進程會頻繁發起到該服務端的短串連,結果導致了兩個問題:
1.大量的TIME_WAIT狀態的串連;
2.發起串連的進程的CPU佔用率接近100%。
這兩個結果嚴重影響了我們網關的效能,在分析具體原因之前,首先做一個提倡,那就是:本機串連本機,首選UNIX域通訊端而不是TCP!
原因其實不需要資料分析,僅僅理論分析就夠了,前提是你要做Linux核心協議棧的IP層處理以及非強制中斷調度有足夠的理解,當然,這些都很簡單。
首先我們來看看問題1。TIME_WAIT就不多說了,只要任何一端主動中斷連線,那麼它最終可能將會進入TIME_WAIT狀態,具體是否會進入在Linux上取決於幾個因素,第一,你有沒有兩端開啟timestamps,如果開啟了,你有沒有在服務端開啟recycle,如果開啟了,那麼TIME_WAIT通訊端就會迅速消失,也就是說,想讓recycle起作用,你一定要開啟timestamps。如果沒有timestamps,那麼就會有大量的TIME_WAIT狀態的通訊端。
在Linux核心協議棧的實現中,所有串連原生資料流,其路由選擇最終都會到定向到loopback,如果你沒有繫結來源IP地址,那麼源/目標IP地址均為127.0.0.1!如果服務連接埠是固定的,那麼最終會接受65535-1個串連,減1的原因在於服務端已經bind了服務連接埠,因此用戶端不能再次bind。這是合理的,因為按照四元組唯一性考慮,一個服務只能接受一個特定IP地址的65535個串連或者65534個串連,但是問題是,如果需求巨大,這顯然不能滿足要求,你要知道,作為伺服器而言,它要考慮的是總的最大並發串連數,一台機器上同時發起6萬多個串連的可能性並不大,因此TCP在大多數情況下是合理,採用16bit的連接埠號碼剛剛好,因為協議頭不能太大,否則載荷率就會變小,這顯然是網路傳輸所要求的,然而本機連本機時,並不需要網路傳輸,你想當然會認為有多少需求就要都要滿足,不過TCP並不適合這種場合。
本機連本機,沒有網路傳輸帶來的延遲,吞吐限制也僅限於本機資源利用,因此並發10萬甚至更多的需求都是合理的,可是TCP並不能滿足,原因就在於它只有16bit的連接埠號碼,目標連接埠固定,同時只能有65534個串連。如何解決呢?我們知道127.0.0.0/8都是屬於loopback的,我們可以採用不同的源IP地址,如果想這麼做,有兩個選擇,那就是要麼用戶端bind源IP為127.x.y.z,要麼SNAT成127.x.y.z,這樣就可以接受海量的串連需求了。但是這並不是最終的解決方案,為什麼非要用TCP呢?TCP本來就是為網路傳輸設計的,它的流控應對不同的主機,擁控應對反覆無常的網路,在本機,這些都不是問題,所以本機連本機,最好使用本機通訊端,比如UNIX域通訊端。
再來看問題2,一個串連原生TCP資料包最終到達了loopback的xmit發送函數,其中簡單的調度了本CPU上的一個非強制中斷處理,然後會在下一次中斷結束後調度其執行,這有很大幾率是在當前發送進程的上下文中進行的,也就是說,發送進程在其上下文中進行了發送操作,而此時非強制中斷借用了其上下文觸發了接收操作,再然後,LOCK的開銷就很明顯,由於大量的TW通訊端的insert和delete,需要頻繁LOCK雜湊表,這種開銷完全記帳到了發送進程的名下,也是不公平的。
注意,Linux核心中,softirq會在兩種上下文中執行,一種是硬體中斷後的任意上下文中,一種是每CPU一個核心線程的上下文中,後者會記帳給top命令的si百分比,前者則會記帳給任意被中斷的進程。