簡介: 解決Tengine健康檢查引起的TIME_WAIT堆積問題
一. 問題背景
“服務上雲後,我們的TCP埠基本上都處於TIME_WAIT的狀態”、“這個問題線上下機房未曾發生過” 這是客戶提交問題的描述。
客戶環境是自建Tengine作為7層反向代理,後端接約1.8萬臺NGINX。Tengine上雲之後,在伺服器上發現大量的TIME_WAIT狀態的TCP socket;由於後端較多,潛在可能影響業務可用性。使用者對比之前的經驗比較擔心是否可能是接入阿里雲之後導致,所以希望我們對此進行詳細的分析。
注:TIME_WAIT狀態的監聽帶來的問題在於主機無法為往外部的連線請求分配動態埠。此時,可以配置net.ipv4.ip_local_port_range,增加其埠選擇範圍(可以考慮 5000 - 65535),但依然存在 2 MSL 時間內被用完的可能。
二. TIME_WAIT原因分析
首先,如果我們重新回顧下TCP狀態機就能知道,TIME_WAIT狀態的埠僅出現在主動關閉連線的一方(跟這一方是客戶端或者是伺服器端無關)。當TCP協議棧進行連線關閉請求時,只有【主動關閉連線方】會進入TIME_WAIT狀態。而客戶的顧慮也在這裡。
一方面,健康檢查使用 HTTP1.0 是短連線,邏輯上應該由後端NGINX伺服器主動關閉連線,多數TIME_WAIT應該出現在NGINX側。
另一方面,我們也通過抓包確認了多數連線關閉的第一個FIN請求均由後端NGINX伺服器發起,理論上,Tengine伺服器的socket 應該直接進入CLOSED狀態而不會有這麼多的TIME_WAIT 。
抓包情況如下,我們根據Tengine上是TIME_WAIT的socket埠號,進行了過濾。
雖然上面的抓包結果顯示當前 Tengine 行為看起來確實很奇怪,但實際上通過分析,此類情形在邏輯上還是存在的。為了解釋這個行為,我們首先應該瞭解:通過tcpdump抓到的網路資料包,是該資料包在該主機上收發的“結果”。儘管在抓包上看,Tengine側看起來是【被動接收方】角色,但在作業系統中,這個socket是否屬於主動關閉的決定因素在於作業系統內TCP協議棧如何處理這個socket。
針對這個抓包分析,我們的結論就是:可能這裡存在一種競爭條件(Race Condition)。如果作業系統關閉socket和收到對方發過來的FIN同時發生,那麼決定這個socket進入TIME_WAIT還是CLOSED狀態決定於 主動關閉請求(Tengine 程式針對 socket 呼叫 close 作業系統函式)和 被動關閉請求(作業系統核心執行緒收到 FIN 後呼叫的 tcp_v4_do_rcv 處理函式)哪個先發生 。
很多情況下,網路時延,CPU處理能力等各種環境因素不同,可能帶來不同的結果。例如,而由於線下環境時延低,被動關閉可能最先發生;自從服務上雲之後,Tengine跟後端Nginx的時延因為距離的原因被拉長了,因此Tengine主動關閉的情況更早進行,等等,導致了雲上雲下不一致的情況。
可是,如果目前的行為看起來都是符合協議標準的情況,那麼如何正面解決這個問題就變得比較棘手了。我們無法通過降低Tengine所在的主機效能來延緩主動連線關閉請求,也無法降低因為物理距離而存在的時延消耗加快 FIN 請求的收取。這種情況下,我們會建議通過調整系統配置來緩解問題。
注:現在的Linux系統有很多方法都可以快速緩解該問題,例如,
a) 在timestamps啟用的情況下,配置tw_reuse。
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_timestamps = 1
b) 配置 max_tw_buckets
net.ipv4.tcp_max_tw_buckets = 5000
缺點就是會往syslog裡寫: time wait bucket table overflow.
由於使用者使用自建 Tengine ,且使用者不願意進行 TIME_WAIT 的強制清理,因此我們考慮通過Tengine的程式碼分析看看是否有機會在不改動 Tengine 原始碼的情況下,改變 Tengine 行為來避免socket被Tengine主動關閉。
Tengine version: Tengine/2.3.1
NGINX version: nginx/1.16.0
1、 Tengine code analysis
從之前的抓包,我們可以看出來多數的TIME_WAIT socket是為了後端健康檢查而建立的,因此我們主要關注 Tengine的健康檢查行為,以下是從ngx_http_upstream_check_module 的開原始碼中摘抄出來的關於socket清理的函式。
從這段邏輯中,我們可以看到,如果滿足以下任一條件時,Tengine會在收到資料包之後直接關閉連線。
- c->error != 0
- cf->need_keepalive = false
- c->requests > ucscf->check_keepalive_requ
這裡,如果我們讓以上的條件變成不滿足,那麼就有可能讓Tengine所在的作業系統先處理被動關閉請求,進行socket清理,進入CLOSED狀態,因為從HTTP1.0的協議上來說,NGINX伺服器這一方一定會主動關閉連線。
2、解決方法
一般情況下,我們對於TIME_WAIT的連線無需太過關心,一般2MSL(預設60s) 之後,系統自動釋放。如果需要減少,可以考慮長連結模式,或者調整引數。
該case中,客戶對協議比較瞭解,但對於強制釋放TIME_WAIT 仍有擔心;同時由於後端存在1.8萬臺主機,長連線模式帶來的開銷更是無法承受。
因此,我們根據之前的程式碼分析,通過梳理程式碼裡面的邏輯,推薦客戶以下健康檢查配置,
check interval=5000 rise=2 fall=2 timeout=3000 type=http default_down=false;
check_http_send "HEAD / HTTP/1.0\r\n\r\n";
check_keepalive_requests 2
check_http_expect_alive http_2xx http_3xx;
理由很簡單,我們需要讓之前提到的三個條件不滿足。在程式碼中,我們不考慮 error 情況,而need_keepalive 在程式碼中預設 enable (如果不是,可以通過配置調整),因此需確保check_keepalive_requests大於1即可進入Tengine的KEEPALIVE邏輯,避免Tengine主動關閉連線。
因為使用HTTP1.0的HEAD方法,後端伺服器收到後會主動關閉連線,因此Tengine建立的socket進入CLOSED狀態,避免進入TIME_WAIT而佔用動態埠資源。
作者:SRE團隊技術小編-小凌
本文為阿里雲原創內容,未經允許不得轉載