一個請求如何被分到某一台後端
負載平衡解決的是「多台都能做這件事時,這一筆該進哪一台」,不是 API 該長什麼樣。
一台機器的 CPU、連線數或記憶體先到頂時,垂直加大規格有上限,也有單點。水平擴展是再放幾台做同一件事的後端。客戶端不該自己寫死 IP:那一台死了,所有人都跟著失敗;那一台比較閒,也沒有人知道。負載平衡器站在客戶端與後端池之間,對每一筆新工作選一台目前被認為健康的後端,並在後端增減時讓呼叫端不用改設定。
客戶端的設定裡通常只有平衡器。後端 IP 可以換、可以增減,只要平衡器的目標群組跟著更新。
這條路徑上有三個容易混在一起的角色。應用程式處理業務。負載平衡器決定同一服務的哪一台複本接到這一筆。API Gateway 決定哪一個服務、哪一版契約、要不要先認證。路由規則與認證留給 API Gateway 與反向代理。這篇只處理複本之間的分配、健康檢查,以及平衡器自己怎麼不變成單點。
面試時先把決策拆成兩層,後面兩節各自展開。第一層是平衡器看得到封包的哪一段:只看到 TCP/UDP 的五元組,還是已經終止連線、看得到 HTTP。第二層是在看得到的資訊上用哪一種分配規則。規則選錯時,後端規格再齊,慢的那一台仍會把尾延遲拉上去。健康檢查通常只會把「沒回應」的摘掉,不會把「還在回 200、只是很慢」的摘掉。
L4 與 L7 各自能做的決策
看得到的欄位決定了排程能用什麼訊號。AWS 用 NLB 與 ALB 拆開這兩層,GCP 用 passthrough Network Load Balancer 與 Application Load Balancer 對照。
L4 平衡器轉送的是連線。它看協議、來源 IP、來源 port、目的 IP、目的 port,用這個五元組做雜湊,讓同一條連線的後續封包回到同一台後端。它不解析 HTTP,因此不能依路徑或標頭把請求分到不同的目標群組,也不能在同一條連線的每個請求上重選後端。L7 平衡器終止客戶端連線(HTTPS 時也終止 TLS),讀到請求列與標頭之後,再替這一筆向後端另開或復用連線。
- 看得到協議與兩端的 IP、port
- 看不到 URL、Header、HTTP 方法
- 執行個體目標上可以保留客戶端來源 IP
- 單條長連線的所有請求釘在同一台
- 看得到 Host、路徑、方法與標頭
- HTTP/1.1 常輪詢;HTTP/2、gRPC 改看未完成請求數
- 來源 IP 變成平衡器的,客戶端 IP 放進轉送標頭
- 同一條連線上的 stream 可以落到不同後端
左欄的決策在連線建立時做完;右欄的決策可以發生在每一個請求。長連線一多,兩邊的尾延遲會差在這裡。
TLS 的分界沿用 HTTP/1.1 與 HTTP/2 那篇的結論就夠:L4 可以在不解密的情況下讀到 ClientHello 裡的 SNI,用來挑憑證或轉送目標;L7 則終止 TLS,看得到明文 HTTP。這篇不再往下拆握手。
| 要比較的點 | AWS NLB(L4) | AWS ALB(L7) | GCP 的對照 |
|---|---|---|---|
| 分配單位 | 連線(TCP/UDP/TLS) | HTTP/HTTPS/gRPC 請求 | passthrough NLB 對連線;Application LB 對請求 |
| 預設怎麼選 | 五元組雜湊 | HTTP/1.1 輪詢;HTTP/2 與 gRPC 用最少未完成請求 | passthrough 用雜湊;Application LB 先依 URL map 選後端服務 |
| 位址 | 每個啟用的 AZ 一個可固定的 IP | 沒有自己的靜態 IP | 全球外部 Application LB 用 anycast,一個入口 |
| 客戶端 IP | 打到執行個體目標時保留來源 IP | 來源變成平衡器,原 IP 在轉送標頭 | passthrough 保留來源;代理型會改寫 |
| 跨 AZ | 預設關閉,打開才把連線送到別的 AZ,並產生跨 AZ 流量 | 一律跨 AZ | 全球 Application LB 本來就跨區域後端;區域型 NLB 預設留在區域內 |
選型先看「決策需要連線還是請求」。需要靜態 IP、極高封包速率、或後端必須看到真實來源 IP 時偏 NLB;需要依請求重新分配、或同一服務上要看 Host/路徑時偏 ALB。URL map 把流量分到不同服務,是閘道的工作,不是這篇的排程。
HTTP/2 與 gRPC 用一條連線承載很多 stream。平衡器若只在連線建立時選一次後端,這位客戶端之後的請求全部釘在同一台,池子裡其他複本幫不上忙。ALB 對這兩種協議改成最少未完成請求(least outstanding requests):每一筆請求都看目前誰手上的請求較少。NLB 看不到請求,做不到這件事。
長連線還有另一個後果:五元組雜湊在連線活著的時候不會因為後端變慢而改道。連線怎麼排空、目標進入 draining 時已建立的連線還能活多久,是即時連線擴展那篇的範圍。這裡只需記住,L4 的「平衡」發生在新連線,不發生在連線中途。
異質後端時分配演算法怎麼傷到尾延遲
後端一樣快時,輪詢就夠。有一台變慢、卻還沒被健康檢查摘除時,輪詢會把尾延遲交給那一台。
Round Robin 依序把新工作分給下一台,筆數幾乎相同。權重(weighted round-robin)改的是筆數比例:Nginx 的 smooth weighted round-robin 會把高權重的主機散開,而不是一口氣塞完再跳過。權重是設定值,不會因為這台突然變慢而自己下降。最少未完成請求看的是此刻誰手上的請求較少;一台變慢時,它的請求停留更久,計數自然升高,新請求就會避開它。Join-the-Shortest-Queue 是對池子裡每一台取最小。Power of Two Choices 只隨機看兩台,把工作放到這兩台裡負載較低的那台。
完全隨機只抽一台時,最滿約是 ln n / ln ln n。多看的那一次抽樣拿走了大部分好處,成本仍是常數。平衡器看得到整個目標群組時,直接做最少未完成請求即可,ALB 在 HTTP/2 就是這樣。客戶端或 sidecar 面對大量 replica、不該每次都詢問全部時,才用兩次抽樣:Envoy 的 least_request 預設 n_choices = 2。一致性雜湊是另一個問題——同一個鍵跨連線都要落到同一台——環與虛擬節點在一致性雜湊那篇,這裡不重推。
| 演算法 | 選擇時看什麼 | 一台變慢但仍回成功 | 常見落點 |
|---|---|---|---|
| Round Robin | 輪到誰 | 慢節點拿到同樣筆數,尾延遲被它綁住 | ALB 的 HTTP/1.1 |
| 加權輪詢 | 設定的權重 | 權重若跟不上變慢的程度,可以比不加權更差 | Nginx upstream、ALB 轉送動作上的 target group 權重 |
| 最少未完成請求 | 此刻還沒做完的請求數 | 慢請求佔著名額,新請求自然少給它 | ALB 的 HTTP/2 與 gRPC |
| Power of Two Choices | 隨機兩台,取負載較低者 | 大叢集上接近全看,觀察成本固定 | Envoy least_request、Finagle |
| 五元組雜湊 | 連線的五元組 | 這條連線不會改道,慢就慢到連線結束 | NLB、GCP passthrough Network LB |
健康檢查通過、只是變慢的後端,五元組雜湊與 Round Robin 都不會少分給它。少分給它的是「看負載」的那一列,不是「看設定」的那一列。
機器比較大就給比較高的權重,這句只在它比較快時成立。規格大但正在 GC、磁碟打滿或鎖競爭的節點,權重越高,分到的筆數越多,尾延遲比 Round Robin 更糟。權重補的是已知的容量差,補不了健康檢查還沒判死的那一段延遲。
六台後端同時在服務。A 是慢節點,B 到 F 每次服務 5 ms。請求每隔 2 ms 到一筆,而且每一筆都能改派——這是 L7 或客戶端平衡,不是 NLB 的五元組。尾延遲是這一批裡最慢那一筆的等待加服務時間。長條是目前這一種演算法下,各台自己的峰值延遲。
預設倍數 4、慢節點權重 1、120 筆時,Round Robin 的尾延遲會遠高於兩次抽樣。把權重拉到 8,加權會反過來比 Round Robin 更差。六台很少,所以兩次抽樣和全看常常一樣;叢集變大時,全看的成本才會逼你改抽兩台。
模擬只有六台,隨機兩台很容易就包含較空的那台,尾延遲會和把六台都看過幾乎相同。這是規模,不是公式失效。ln ln n 的優勢在上百台、而且觀察每一台都要付網路成本時才明顯。中央平衡器付得起全看;客戶端平衡通常付不起。
健康檢查,以及平衡器自己的單點
檢查回答的是「還通不通」。平衡器若只有一台,後端池再健康,入口死了全部請求都進不來。
主動健康檢查由平衡器週期性探測目標:TCP 能否握手,或 HTTP 是否回期望的狀態碼。連續失敗達到不健康門檻就停止送新工作;連續成功達到健康門檻才加回。被動檢查則是看真實流量的連續錯誤(Envoy 的 outlier detection),不必乾等下一輪探測。AWS 的目標群組以主動檢查為主。
Application Load Balancer 目標群組的常見預設是間隔 30 秒、連續失敗 2 次摘除、連續成功 5 次才加回。偵測窗口大約一分鐘,恢復比摘除更慢,這是故意的:剛恢復的節點不要立刻吃回全部流量。NLB 的 TCP 檢查逾時通常比 HTTP 長,門檻不要照抄 ALB 的數字,以那個目標群組的現值為準。摘除之後,已經接上的請求還能做完的時間是 deregistration delay(目標群組預設常常是 300 秒)。長連線怎麼排空見即時連線擴展。
sequenceDiagram participant LB as 平衡器 participant T as 後端 LB->>T: 主動探測 T--xLB: 逾時 Note over LB: 失敗計數加一 LB->>LB: 達門檻就摘除 LB->>T: 間隔後再探 T-->>LB: 回應成功 LB->>LB: 達門檻就加回
摘除與加回都看連續次數,不看單一次。中間那台後端在被摘除期間不再收到新請求,但探測仍繼續打。
不健康門檻設成 1、間隔設成 1 秒,一次 GC pause 就會摘除節點。剩下的節點瞬間變忙,它們的檢查也開始逾時,摘除會連鎖。慢但仍回 200 的節點不該用縮短 timeout 來「判死」——檢查回答的是失敗,第 3 節的排程才回答慢。真的要依延遲摘除,得另做延遲異常的門檻,並且加回要慢、要抖動,避免所有客戶端同一秒湧回。
平衡器自己的可用性分兩種做法。託管服務把資料面鋪在你啟用的每個可用區:NLB 每個 AZ 一個 IP,DNS 回多筆位址;某一區的節點故障時,還握著那個 IP 的客戶端會失敗,必須改連另一個位址。這段重試屬於逾時、重試與退避,不是把 NLB 再做成主備兩台虛擬 IP 就結束。ALB 的 DNS 回的是 AWS 持有的位址,位址會變、TTL 短,客戶端同樣要能重連。自建單台 HAProxy 或 Nginx 則是真的單點:要嘛兩台加 VRRP,要嘛前面再放一層會做健康檢查的 DNS/anycast。控制面短暫故障時,既有連線多半還在;新目標註冊與健康狀態的更新會停,這和資料面整台消失不是同一種事故。
跨 AZ 是流量與故障域的交換。NLB 預設不跨 AZ:一個 AZ 的目標少,打到該 AZ IP 的連線就集中在較少的目標上,單機更熱,但流量不跨區計費。打開跨 AZ 後負載較勻,跨區流量要付錢,而且一個 AZ 的目標故障時另一個 AZ 會立刻多接。ALB 沒有這個開關,一律跨 AZ。目標群組若只放在一個 AZ,多 AZ 的平衡器仍然是單邊後端,入口的冗餘是假的。
客戶端平衡,以及不該放進平衡器的事
呼叫端自己選複本可以少一跳,但每個客戶端只看得到自己的視角。業務路由與認證不應因此塞進平衡器。
客戶端平衡把第 3 節的選擇搬進呼叫端或旁邊的 sidecar。服務發現(Cloud Map、DNS、或控制面下發的端點清單)告訴它目前有哪些複本,它再用最少未完成請求或兩次抽樣挑一台。少掉集中式代理的那一跳,也少一個必須容量規劃的入口。代價是視角不完整:每個客戶端的未完成請求數只統計自己發出的連線,叢集的真實負載它看不到,容易一起湧向剛恢復、看起來最空的那台。端點清單若靠 DNS,TTL 內仍會打到已摘除的 IP。清單要嘛短 TTL 加客戶端重試,要嘛改由會推送健康狀態的控制面更新。
Mesh sidecar 在這個題目裡就是貼在行程旁的客戶端平衡器。私有 CA、SPIFFE、mTLS 屬於服務間安全,不要在選複本的時候一併設計信任域。
| 面試官問 | 先給的取捨 |
|---|---|
| 什麼時候用 NLB,不使用 ALB? | 要靜態 IP、保留來源 IP、非 HTTP、或連線速率高到不想解析請求。不要用它做路徑路由。 |
| HTTP/2 為什麼不能 Round Robin? | 輪詢若發生在連線上,一條連線的所有 stream 釘死一台。要在請求層做最少未完成請求。 |
| 一台變慢但健康檢查是綠的? | 檢查只看通不通。改看負載的排程,或另做延遲摘除;不要把探測 timeout 縮到 SLO。 |
| 平衡器掛了怎麼辦? | 託管的資料面在多個 AZ。客戶端要能換掉失敗的位址。自建則不要只留一個行程。 |
| 權重怎麼設? | 對已知的容量差設權重。權重不會跟著瞬間延遲走;設反了比 Round Robin 更糟。 |
這五題都落在「連線還是請求、筆數還是負載、通不通還是慢不慢」。認證、配額與多服務編排不在表裡。
不該放進負載平衡器的,是會讓它變成業務系統的那一層:依身分決定能不能呼叫、把多個後端的回應組裝成一個 API、依租戶做配額。配額的演算法在限流演算法。契約與版本在 API 設計風格與演進。這些放進平衡器之後,每一次規則變更都要動資料面,故障域也從「某一台複本」變成「所有服務的入口」。