健康檢查路由設計
健康檢查路由決定負載平衡把流量送給哪些後端、不送給哪些。設計沿兩個時間窗口展開:一個新後端要多久才開始接流量、一個壞後端要多久才被移出輪替。兩個窗口都可能設錯,且錯的方向相反——太寬鬆,壞掉的後端留在輪替裡繼續讓部分使用者收到錯誤;太嚴格,部署瞬間還在初始化的新後端就被判死,反覆移出移入、使用者看到間歇性失敗。
這一章講的是負載平衡端怎麼消費健康訊號(多久探一次、幾次算數、探到不健康怎麼辦)。服務端怎麼提供健康訊號、端點回什麼才算健康、check 要探到多深,是 模組四的 health check endpoint 設計——服務怎麼提供跟 LB 怎麼使用是同一個概念的兩側,這裡不重述服務側。
被動探測與主動探測
負載平衡判斷後端健康有兩種路徑。主動探測是 LB 定時對後端發一個健康請求(例如每 15 秒 curl 一次 /healthz),依回應判定健康;被動探測是 LB 觀察它轉發的真實流量、不額外發請求——某個後端連續回幾次錯誤或逾時,就把它標成不健康、暫停送流量。
兩者的盲點不同。主動探測的盲點是探測請求跟真實請求走的路徑可能不同:/healthz 通了不代表真實的業務端點通,一個只檢查淺層的探測會漏掉只在真實流量上出現的失敗。被動探測的盲點是它要先犧牲幾個真實請求才發現問題——後端壞掉的當下,是靠幾個真實使用者撞到錯誤,LB 才學到它不健康。成熟的做法常常兩者都用:主動探測抓「後端整個沒回應」,被動探測抓「後端回應了但一直出錯」。雲端 LB(如 ALB)預設走主動探測,開源 nginx 預設走被動探測,主動探測要靠額外機制,這個差別在 nginx 實務配置 展開。
閾值定出兩個時間窗口
主動探測的行為由幾個參數的交互決定,它們算術地定出前面說的兩個窗口。以一組具體的 ALB 設定為例:interval = 15(每 15 秒探一次)、healthy_threshold = 2(連續 2 次通過才算健康)、unhealthy_threshold = 3(連續 3 次失敗才算壞)、timeout = 5(單次探測 5 秒沒回算失敗)。
這組參數的意思是:一個新後端要等 2 × 15 = 30 秒(兩次通過)才開始接流量,一個壞掉的後端要 3 × 15 = 45 秒(三次失敗)才被移出。每個參數的鬆緊都是雙向取捨:
| 參數 | 過小的風險 | 過大的風險 | 起點建議 |
|---|---|---|---|
interval | 探測本身對後端造成額外負擔 | 壞後端被偵測到的延遲增加 | 15-30 秒 |
healthy_threshold | 還沒完全就緒就接流量 | 部署後等太久才開始分流 | 2-3 次 |
unhealthy_threshold | 暫時性波動導致健康的後端被移出 | 壞後端繼續收流量太久 | 2-3 次 |
timeout | 正常但偏慢的回應被誤判為失敗 | 確實掛了卻要等很久才確認 | 5 秒 |
unhealthy_threshold 設 1 的問題最典型:一次網路抖動、一個偶發的慢回應,就足以把一個健康的後端踢出去,造成不必要的容量波動。要幾次連續失敗才算數,是在「快速摘除壞後端」跟「容忍暫時性抖動」之間取平衡。
interval 的取捨在探測頻率與探測負擔之間:探得太密,健康檢查本身變成後端的額外負載(每台都要固定回應探測);探得太疏,壞後端要更久才被偵測到。timeout 則要對齊後端正常回應的時間分佈:設得比正常的慢回應還短,會把偶爾偏慢但其實健康的後端誤判成失敗、把它踢出去;設得太長,一台確實掛了的後端要拖很久才確認。這兩個參數跟兩個 threshold 相乘,才共同定出前面那兩個窗口的實際長度。
flapping:部署瞬間的移出移入
健康檢查最常見的失效表現是 flapping——每次部署時,新後端在健康與不健康之間反覆跳動。根因幾乎都是同一個:應用的啟動時間超過了「開始接流量」的窗口。新容器起來、應用還在初始化(載入索引、建連線池),但健康檢查已經開始探測,探到還沒就緒的應用、判不健康、移出;應用初始化完又通過、移入;下一次探測可能又撞上某個還沒好的部分。
判讀方式是觀察後端在健康與不健康之間的轉換次數。每次部署都看到新後端跳動,代表初始等待不夠——應用的啟動時間超出了 healthy_threshold × interval。兩個解法:加大 healthy_threshold(拉長開始接流量前的等待),或設一段啟動寬限期(像 ECS 的 startPeriod)讓健康檢查在應用初始化期間暫停、不把初始化中的失敗計入。啟動期該跟穩定運行期用不同的容忍窗口,這對應 模組四 startup 探針 的語意:啟動中是一種獨立的健康狀態,不該用穩定期的標準去判。
健康檢查決定後端清單,但清單會滯後
在編排平台上,健康檢查跟服務發現是綁定的:Kubernetes 把 endpoint 跟 readiness 探針綁定,一個 pod 的 readiness 通過,它的 IP 才被加進 Endpoints、才進入 LB 的後端清單。readiness 的定義因此直接決定了流量會不會進來——這是「健康檢查決定後端清單」最直接的機制。
LB 拿到的清單會滯後於真實健康狀態,中間有一段傳播延遲。readiness 從通過轉為失敗,要先傳到 endpoint controller、再傳到每個節點的轉發層(kube-proxy 的 iptables/IPVS 或 envoy),這段期間客戶端仍可能打到已經 not-ready 的 pod。這也是為什麼摘除一個實例時,要在真正關閉前留一段等待(preStop 加 5 到 15 秒),讓摘除狀態傳播到所有轉發層再收束——這段的完整處理在 模組四 graceful shutdown 的退場順序。用 DNS 做服務發現時滯後更明顯:DNS A record 不帶健康狀態,回的 IP 可能對應一個已經 not-ready 但還沒被移除的後端,加上 DNS TTL 與客戶端快取,健康狀態的更新要更久才傳到。這裡有個特別危險的邊界——TTL 常見預設 30 秒,但某些 JVM 客戶端的 DNS 快取預設是永不過期,一旦快取到一個壞掉的 IP 就再也不重查,滯後從「幾十秒」變成「直到重啟」。
LB 顯示 healthy 不等於服務可服務
健康檢查通過是一個節點層的訊號,它不保證那個節點能承受當前的流量。最常見的誤判是把「LB 顯示節點 healthy」當成「服務可承受流量」——健康檢查通常只是一個定期的淺探測通過,跟「這個節點扛得住突然湧入的重連潮」是不同層級的問題。
事故裡要把兩層訊號分開看。節點層健康是健康檢查 pass;連線層健康是重連率、長連線錯誤率、tail latency 這些反映「節點實際承受得了流量嗎」的訊號。一個剛通過健康檢查、被 LB 加回輪替的節點,可能立刻被積壓的重連潮壓垮——健康檢查說它好了,連線層訊號說它扛不住。health contract 要盡量反映服務的真實可服務狀態(含依賴連線池、必要 config、關鍵背景任務),而不是停在單一淺探測成功;但即使如此,節點層跟連線層仍是兩個要分開判讀的訊號。這條判讀在切流事故裡尤其關鍵,完整脈絡見 backend 的 load balancer 合約。
下一步路由
- 服務端怎麼提供健康訊號、端點探多深 → 模組四 Health check endpoint 設計
- 在 nginx 上配被動與主動健康檢查、以及開源版的限制 → nginx 實務配置
- 健康檢查通過的實例,怎麼被演算法選中 → 負載分散演算法
- 摘除實例時的傳播延遲與退場順序 → 模組四 Graceful shutdown