nginx 反向代理 kuberntes service 出現 502 問題排查


更好的閱讀體驗建議點擊下方原文鏈接。
原文地址:http://maoqide.live/post/problems/kubernetes-service-502/

問題背景和現象

當前 Kuberntes 集群使用 calico 作為 CNI 組件,並使用 BGP 模式將 pod IP 和 Service IP 與集群外網絡打通,通過集群外的 nginx 作反向代理對外提供服務,應用都是以 Deployment 形式部署。通過一段時間的觀察,部分應用反饋,在應用發布后一段時間內,服務有一定幾率出現 502 報錯。

問題排查

最直接的猜測,是否問題只發生在滾動更新過程中,即應用沒有做好檢查檢測的配置,導致服務沒有真正可用,Pod 卻已經處於 ready 狀態。
簡單的測試后很快排除這個可能,對配置了有效健康檢查探針的 Deployment 進行滾動更新,並使用 ab 通過 nginx 配置的域名進行持續請求(此時無並發),發現在應用滾動更新結束后,並通過 pod IP 人工確認了服務沒有問題,仍有概率出現 502 錯誤,且出現錯誤的現象會持續幾分鍾甚至十幾分鍾的時間,顯然遠遠超過了滾動更新所需時間。

上面的初步測試的現象,排除了應用本身的問題。下一個懷疑的目標指向了 nginx。既然現象是通過 nginx 代理訪問產生的,那么直接請求 Service 有沒有問題呢,由於當前集群 Service 地址和外部網絡做了打通,測試起來很方便,我准備了如下的測試:

  1. ab 持續請求域名通過 nginx 訪問服務,並觸發滾動更新(ab -r -v 2 -n 50000 http://service.domain.com/test
  2. ab 持續請求 serviceIP:port 訪問服務,並觸發滾動更新(ab -r -v 2 -n 50000 http://10.255.10.101/test

經過測試,案例 1 出現了 502 錯誤,案例 2 未出現。所以,問題是在 nginx 嘛?
找到負責 nginx 的同事進行分析,結論是 nginx 似乎不會造成類似的問題。那為什么上面測試中只有案例1 復現了問題呢?於是我決定重新進行測試,這次在 ab 請求的時候加上了並發(-c 10),結果,兩個案例都出現了 502 的錯誤。這樣,問題似乎又回到了 K8S 集群本身,而且似乎在請求量較大的情況下才會出現。

這時,我開始懷疑是否可能是因為某種原因,滾動發布后的一段時間里,一些請求會錯誤的被分發到已經被殺掉的老得 podIP 上。為了驗證這一猜測,我進行了如下實驗:

  1. 創建一個測試的 Deployment,副本數為 1,提供簡單的 http 服務,並在接收到請求時輸出日志,並創建對應 Service。
  2. 使用 ab 並發請求該服務 Service 地址。
  3. 使用 kubectl patch 修改 Pod 的 label,使其和 Deployment 不一致,觸發 Deployment 自動拉起一個新的 Pod。
  4. 追蹤新的 Pod 和老的 Pod 的日志,觀察請求進來的情況。

第三步 patch pod 的 label,是為了保留原來的 pod 實例,以便觀察請求是否會分發到老的 Pod。(patch Pod 的 label 不會使 Pod 重啟或退出,但是改變了 label,會使 Pod 脫離原 Deployment 的控制,因此觸發 Deployment 新建一個 Pod)。
結果和預期一致,當新的 Pod 已經 ready,Endpoint 已經出現了新的 Pod 的 IP,請求仍然會進到原來的 Pod 中。
基於以上的結果,又通過多次實驗,觀察 K8S 節點上的 IPVS 規則,發現在滾動更新及之后一段時間,老的 podIP 還會出現在 IPVS 規則中,不過 weight 為 0,手動刪除后 weight 為 0 的 rs 后,問題就不再出現。到此,找到問題所在是 IPVS,但是為什么會這樣呢,在搜索了相關的文章后,大概找到了原因。
詭異的 No route to host,講到了 IPVS 的一個特性:

也就是 IPVS 模塊處理報文的主要入口,發現它會先在本地連接轉發表看這個包是否已經有對應的連接了(匹配五元組),如果有就說明它不是新連接也就不會調度,直接發給這個連接對應的之前已經調度過的 rs (也不會判斷權重);如果沒匹配到說明這個包是新的連接,就會走到調度這里 (rr, wrr 等調度策略)。    

即:五元組(源IP地址、目的IP地址、協議號、源端口、目的端口)一致的情況下,IPVS 有可能不經過權重判斷,直接將新的連接當成存量連接,轉發到原來的 real server(即 PodIP)上。理論上這種情況在單一客戶端大量請求的場景下,才有可能觸發,這也是詭異的 No route to host一文中模擬出的場景,即:

不同請求的源 IP 始終是相同的,關鍵點在於源端口是否可能相同。由於 ServiceA 向 ServiceB 發起大量短連接,ServiceA 所在節點就會有大量 TIME_WAIT 狀態的連接,需要等 2 分鍾 (2*MSL) 才會清理,而由於連接量太大,每次發起的連接都會占用一個源端口,當源端口不夠用了,就會重用 TIME_WAIT 狀態連接的源端口,這個時候當報文進入 IPVS 模塊,檢測到它的五元組跟本地連接轉發表中的某個連接一致(TIME_WAIT 狀態),就以為它是一個存量連接,然后直接將報文轉發給這個連接之前對應的 rs 上,然而這個 rs 對應的 Pod 早已銷毀,所以抓包看到的現象是將 SYN 發給了舊 Pod,並且無法收到 ACK,伴隨着返回 ICMP 告知這個 IP 不可達,也被應用解釋為 “No route to host”。    

原因分析

這里分析一下之前的測試中為何會出現兩種不同的結果。我一共進行了兩次對比實驗。
第一次,未加並發,通過 nginx 和 通過 Service IP 進行訪問並對比。這組實驗中,通過 nginx 訪問復現了問題,而通過 Service IP 沒有,這個結果也險些將排查引入歧途。而現在分析一下,原因是因為目前的 K8S 服務訪問入口的設計,是集群外 nginx 為整個 K8S 集群共用,所以 nginx 的訪問量很高,這也導致 nginx 向后端的 upstream(即 Service IP)發起連接時,理論上源端口重用的概率較高(事實上經過抓包觀察,確實幾分鍾內就會觀察到多次端口重用的現象),因而更容易出現五元組重復的情況。
第二次,同樣的對比,這次加了並發,兩邊的案例都復現了問題。這樣,和上面文章中的場景類似,由於加了並發,發布 ab 請求的機器,也出現了源端口不足而重用的情況,因此也復現了問題。
而正式環境出現的問題反饋,和我第一次實驗通過 nginx 訪問得到復現,是同一個原因,雖然單個應用的請求量遠沒有達到能夠觸發五元組重復的量級,但是集群中的所有應用請求量加起來,就會觸發此問題。

解決方案

幾種解決方案,上面引用的文章中也都提到,另外可參考isuue 81775,對這一問題及相關的解決方式有很多的討論。
鑒於目前我們的技術能力和集群規模,暫時無法也無需進行 linux 內核級別的功能修改和驗證,並且調研了業務應用,絕大部分以短連接為主,我們采用了一種簡單直接的方式,在一定程度上避免該問題。開發一個自定義的進程,並以 Daemonset 的方式部署在每個 k8s 的每個節點上。該進程通過 informer 機制監聽集群 Endpoint 的變化,一旦監聽到事件,便獲取 Endpoint 及其對應 Service 的信息,並由此找到其在本節點上對應產生的 IPVS 規則,如果發現在 Virtual Service 下有 weight 為 0 的 Real Service,則立即刪除此 Real Service。但是這一解決方式,不可避免的犧牲了部分優雅退出的特性。但是在綜合了業務應用的特點權衡之后,這確實是目前可接受的一種解決方式(雖然極其不優雅)。

思考

是否應該如此使用 Service?
總結問題的原因,在我們單一業務的請求量遠未達到會觸發五元組重復這種小概率事件的瓶頸時,過早的遭遇這一問題,和我們對 K8S 服務的入口網關的設計有很大關系,打通 Service 和虛擬機的網絡,使用外部 nginx 作為入口網關,這種用法,在 K8S 的實踐中應該算是非常特殊(甚至可稱為奇葩),但是這一設計,也是由於目前業務的實例,存在大量虛擬機和容器混布的場景。教訓是,在推廣和建設 k8s 這種復雜系統時,盡量緊靠社區及大廠公開的生產最佳實踐,減少僅憑經驗的或機械延用的方式進行架構設計,否則很容易踩坑,事倍功半。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM