
1. 問題背景
“服務上雲後,我們的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 時間内被用完的可能。
2. 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端口号,進行了過濾。
圖1:一次HTTP請求互動過程
雖然上面的抓包結果顯示目前 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
2.1 Tengine code analysis
從之前的抓包,我們可以看出來多數的TIME_WAIT socket是為了後端健康檢查而建立的,是以我們主要關注 Tengine的健康檢查行為,以下是從ngx_http_upstream_check_module 的開源代碼中摘抄出來的關于socket清理的函數。
圖2:Tengine 健康檢查完成後清理socket過程
從這段邏輯中,我們可以看到,如果滿足以下任一條件時,Tengine會在收到資料包之後直接關閉連接配接。
- c->error != 0
- cf->need_keepalive = false
- c->requests > ucscf->check_keepalive_requ
圖3: Tengine 中真正完成socket關閉的函數
這裡,如果我們讓以上的條件變成不滿足,那麼就有可能讓Tengine所在的作業系統先處理被動關閉請求,進行socket清理,進入CLOSED狀态,因為從HTTP1.0的協定上來說,NGINX伺服器這一方一定會主動關閉連接配接。
2.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主動關閉連接配接。
圖4:Tengine健康檢查參考配置
因為使用HTTP1.0的HEAD方法,後端伺服器收到後會主動關閉連接配接,是以Tengine建立的socket進入CLOSED狀态,避免進入TIME_WAIT而占用動态端口資源。
參考文檔
[1] ngx_http_upstream_check_module:
https://tengine.taobao.org/document/http_upstream_check.html我們是阿裡雲智能全球技術服務-SRE團隊,我們緻力成為一個以技術為基礎、面向服務、保障業務系統高可用的工程師團隊;提供專業、體系化的SRE服務,幫助廣大客戶更好地使用雲、基于雲建構更加穩定可靠的業務系統,提升業務穩定性。我們期望能夠分享更多幫助企業客戶上雲、用好雲,讓客戶雲上業務運作更加穩定可靠的技術,您可用釘釘掃描下方二維碼,加入阿裡雲SRE技術學院釘釘圈子,和更多雲上人交流關于雲平台的那些事。