01

一個請求如何被分到某一台後端

負載平衡解決的是「多台都能做這件事時,這一筆該進哪一台」,不是 API 該長什麼樣。

一台機器的 CPU、連線數或記憶體先到頂時,垂直加大規格有上限,也有單點。水平擴展是再放幾台做同一件事的後端。客戶端不該自己寫死 IP:那一台死了,所有人都跟著失敗;那一台比較閒,也沒有人知道。負載平衡器站在客戶端與後端池之間,對每一筆新工作選一台目前被認為健康的後端,並在後端增減時讓呼叫端不用改設定。

Client
只認得平衡器的位址
▸
DNS
把名字解成平衡器位址
▸
Load Balancer
在健康後端裡選一台
▸
Backend
真正執行請求

客戶端的設定裡通常只有平衡器。後端 IP 可以換、可以增減,只要平衡器的目標群組跟著更新。

這條路徑上有三個容易混在一起的角色。應用程式處理業務。負載平衡器決定同一服務的哪一台複本接到這一筆。API Gateway 決定哪一個服務、哪一版契約、要不要先認證。路由規則與認證留給 API Gateway 與反向代理。這篇只處理複本之間的分配、健康檢查,以及平衡器自己怎麼不變成單點。

面試時先把決策拆成兩層,後面兩節各自展開。第一層是平衡器看得到封包的哪一段:只看到 TCP/UDP 的五元組,還是已經終止連線、看得到 HTTP。第二層是在看得到的資訊上用哪一種分配規則。規則選錯時,後端規格再齊,慢的那一台仍會把尾延遲拉上去。健康檢查通常只會把「沒回應」的摘掉,不會把「還在回 200、只是很慢」的摘掉。

一句話答法負載平衡是在一組等價後端之間為每一筆工作選一台,並把不健康的排除;它不決定 API 契約,也不取代業務服務。
02

L4 與 L7 各自能做的決策

看得到的欄位決定了排程能用什麼訊號。AWS 用 NLB 與 ALB 拆開這兩層,GCP 用 passthrough Network Load Balancer 與 Application Load Balancer 對照。

L4 平衡器轉送的是連線。它看協議、來源 IP、來源 port、目的 IP、目的 port,用這個五元組做雜湊,讓同一條連線的後續封包回到同一台後端。它不解析 HTTP,因此不能依路徑或標頭把請求分到不同的目標群組,也不能在同一條連線的每個請求上重選後端。L7 平衡器終止客戶端連線(HTTPS 時也終止 TLS),讀到請求列與標頭之後,再替這一筆向後端另開或復用連線。

L4 · 連線
五元組雜湊,連線期間不改道
  • 看得到協議與兩端的 IP、port
  • 看不到 URL、Header、HTTP 方法
  • 執行個體目標上可以保留客戶端來源 IP
  • 單條長連線的所有請求釘在同一台
L7 · 請求
每個請求都能重選後端
  • 看得到 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 不能沿用連線級輪詢

HTTP/2 與 gRPC 用一條連線承載很多 stream。平衡器若只在連線建立時選一次後端,這位客戶端之後的請求全部釘在同一台,池子裡其他複本幫不上忙。ALB 對這兩種協議改成最少未完成請求(least outstanding requests):每一筆請求都看目前誰手上的請求較少。NLB 看不到請求,做不到這件事。

長連線還有另一個後果:五元組雜湊在連線活著的時候不會因為後端變慢而改道。連線怎麼排空、目標進入 draining 時已建立的連線還能活多久,是即時連線擴展那篇的範圍。這裡只需記住,L4 的「平衡」發生在新連線,不發生在連線中途。

一句話答法L4 用五元組把整條連線釘死,換來源 IP 與高 pps;L7 終止連線後按請求重選,HTTP/2 必須用未完成請求數,否則多路復用會把一個客戶端釘在一台後端上。
03

異質後端時分配演算法怎麼傷到尾延遲

後端一樣快時,輪詢就夠。有一台變慢、卻還沒被健康檢查摘除時,輪詢會把尾延遲交給那一台。

Round Robin 依序把新工作分給下一台,筆數幾乎相同。權重(weighted round-robin)改的是筆數比例:Nginx 的 smooth weighted round-robin 會把高權重的主機散開,而不是一口氣塞完再跳過。權重是設定值,不會因為這台突然變慢而自己下降。最少未完成請求看的是此刻誰手上的請求較少;一台變慢時,它的請求停留更久,計數自然升高,新請求就會避開它。Join-the-Shortest-Queue 是對池子裡每一台取最小。Power of Two Choices 只隨機看兩台,把工作放到這兩台裡負載較低的那台。

d = 2 時,最滿箱子的負載約為 ln ln n / ln 2

完全隨機只抽一台時,最滿約是 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 的五元組。尾延遲是這一批裡最慢那一筆的等待加服務時間。長條是目前這一種演算法下,各台自己的峰值延遲。

Round Robin 尾延遲 0 ms
加權尾延遲 0 ms
兩次抽樣尾延遲 0 ms
全看最短佇列尾延遲 0 ms
A · 慢節點的峰值 其餘後端的峰值
A 0
B 0
C 0
D 0
E 0
F 0

預設倍數 4、慢節點權重 1、120 筆時,Round Robin 的尾延遲會遠高於兩次抽樣。把權重拉到 8,加權會反過來比 Round Robin 更差。六台很少,所以兩次抽樣和全看常常一樣;叢集變大時,全看的成本才會逼你改抽兩台。

關鍵兩次抽樣在小池子裡看起來「沒有比全看好」

模擬只有六台,隨機兩台很容易就包含較空的那台,尾延遲會和把六台都看過幾乎相同。這是規模,不是公式失效。ln ln n 的優勢在上百台、而且觀察每一台都要付網路成本時才明顯。中央平衡器付得起全看;客戶端平衡通常付不起。

一句話答法後端速度不同時,輪詢把同樣筆數送給慢節點,尾延遲被它決定;看未完成工作或只抽兩台取較空者,會少分給它。權重是靜態的,設反了比不加重更糟。
04

健康檢查,以及平衡器自己的單點

檢查回答的是「還通不通」。平衡器若只有一台,後端池再健康,入口死了全部請求都進不來。

主動健康檢查由平衡器週期性探測目標:TCP 能否握手,或 HTTP 是否回期望的狀態碼。連續失敗達到不健康門檻就停止送新工作;連續成功達到健康門檻才加回。被動檢查則是看真實流量的連續錯誤(Envoy 的 outlier detection),不必乾等下一輪探測。AWS 的目標群組以主動檢查為主。

Tdetect ≈ unhealthy threshold × interval

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: 達門檻就加回
  

摘除與加回都看連續次數,不看單一次。中間那台後端在被摘除期間不再收到新請求,但探測仍繼續打。

陷阱把檢查調到跟 SLO 一樣敏感

不健康門檻設成 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 的平衡器仍然是單邊後端,入口的冗餘是假的。

一句話答法健康檢查用連續失敗次數乘上間隔換偵測時間,門檻太低會連鎖摘除;託管平衡器的單點在「客戶端握著一個已經死掉的 AZ 位址」,自建單行程式才是整台入口只有一個行程。
05

客戶端平衡,以及不該放進平衡器的事

呼叫端自己選複本可以少一跳,但每個客戶端只看得到自己的視角。業務路由與認證不應因此塞進平衡器。

客戶端平衡把第 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 設計風格與演進。這些放進平衡器之後,每一次規則變更都要動資料面,故障域也從「某一台複本」變成「所有服務的入口」。

一句話答法客戶端平衡用服務發現加本地的兩次抽樣或最少未完成請求,省下一跳,但每個客戶端只有局部負載;集中式 L4/L7 平衡器只負責等價複本的選擇與健康,認證與多服務路由留給閘道。