解決Tengine健康檢查引起的TIME_WAIT堆積問題

  • 2021 年 1 月 18 日
  • 筆記

簡介: 解決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端口號,進行了過濾。

 

圖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

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、解決方法

一般情況下,我們對於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而佔用動態端口資源。

作者:SRE團隊技術小編-小凌

原文鏈接

本文為阿里雲原創內容,未經允許不得轉載