下一個請求該送給後端群組裡的哪個實例?負載分散演算法就是在回答這件事,選擇沿兩個維度展開——這個演算法看不看實例當前的負載狀態、以及它需不需要把同一來源固定綁到同一實例(親和性)。這兩個維度決定了一個演算法在什麼流量型態下分得均勻、什麼型態下會製造熱點。

每個演算法各適合一種流量型態,沒有一個在所有情境都最好。均勻同質的請求適合最簡單的輪流,成本差異大的請求需要看當前負載,有快取或會話親和性需求的則需要雜湊。選錯的代價是負載不均:一部分實例過載、一部分閒置,整體容量被最忙的那台限制住。

Round-robin:假設同質均勻的輪流

Round-robin 依序把請求輪流分給每個實例,不看任何實例的當前狀態。它成立的前提是兩個同質假設:後端實例規格一致、每個請求的處理成本相近。這兩個假設成立時,輪流就能讓負載自然均勻,且實作最簡單、沒有需要維護的狀態。

實例規格不一致時,用加權輪流(weighted round-robin)補——規格大的實例配高權重,分到的請求比例對應它的處理能力。加權處理的是「實例不同質」,但它仍不看請求成本:每個請求被當成等重來分。所以當請求成本差異大(有的請求 10 毫秒回、有的要跑 5 秒的查詢),輪流不管加不加權都會失準——一台實例連續接到幾個重請求就過載了,但輪流還是按順序繼續往它送。

Least-connections:看當前負載的分配

Least-connections 把請求送給當前連線數最少的實例,用「連線數」當實例忙碌程度的近似。它適合請求成本差異大的流量:重請求會讓一台實例的連線數維持得高,演算法就自動少送新請求給它,把流量導向連線數低(比較閒)的實例。輪流看不到的當前負載,least-connections 用連線數看到了。

連線數是負載的近似、不是負載本身。長連線場景(WebSocket、SSE)下,一個連線數不代表一個當前工作量——一個閒置的長連線也算一條連線。這時連線數會高估閒置連線多的實例的負載。要更準的話得看實際的處理指標(in-flight 請求數、CPU),但那需要實例回報更多狀態,成本更高。連線數是「便宜且多數時候夠用」的折衷。

看負載的兩個變體:P2C 與 least-time

least-connections 在大規模或分散式的負載平衡上有個弱點:它要維護所有實例的全域連線數狀態,跨多個 LB 節點時這個全域狀態很貴、也容易不同步,還會讓每個 LB 都往「當前最閒的那一台」擠、造成羊群效應。power-of-two-choices(P2C)用一個便宜的近似解掉這個問題——隨機挑兩台、把請求送給其中連線數較少的那台。它幾乎不需要全域狀態(只比兩台),效果卻接近完整的 least-connections,同時避開了羊群效應。這是 Envoy 與多數 service mesh 的預設演算法。

least-response-time(least-time)則換一個負載近似:用實例的回應時間、而不是連線數。一台連線數低、但每個請求都跑得慢的實例,least-connections 會繼續送、least-time 會避開它——因為它直接以延遲衡量「這台現在扛得動嗎」。nginx Plus、HAProxy 都支援,代價是要持續量測各實例的回應時間。連線數、in-flight 請求、回應時間,是同一條光譜上由便宜到精準的三個負載近似。

Hash-based:需要親和性時的固定映射

當同一來源的請求需要固定落到同一實例時,用雜湊。IP hash 依 client IP 算雜湊、固定分到某個實例,讓同一個客戶端的請求總是打到同一台。它提供一種不需要外部儲存的會話親和性——會話狀態存在那台實例的記憶體裡,靠 IP hash 保證後續請求回到同一台。

IP hash 有兩個要注意的失準點。一是分佈不均:大量客戶端藏在同一個 NAT 或 proxy 後面時,它們對外是同一個 IP,會被雜湊到同一台實例,造成熱點。二是後端增減時的大規模重新映射——實例數量一變,雜湊的模數變了,大部分 IP 的落點都會改變,記憶體裡的會話狀態集體失效。

Consistent hash 解的是第二個問題。它把實例排在一個雜湊環上,一個 key 落到環上順時針的下一個實例;增減一台實例時,只有環上相鄰的一小段 key 需要重新映射,其餘不動。這讓它適合有快取親和性的場景——同一個 key 固定打到同一台、命中那台的本地快取,且擴縮容時不會讓整片快取失效。代價是實作比取模雜湊複雜,且要用虛擬節點來讓 key 在環上分佈得夠均勻。

Sticky session:親和性的會話層實作

Sticky session(會話黏著)是親和性的另一種實作,把同一個會話綁定到同一實例,常見做法是 LB 發一個 cookie 標記該會話該去哪台。它跟 IP hash 解同一類問題(會話狀態留在實例本地、需要後續請求回到同一台),但綁定的粒度是會話而非 IP,避開了 NAT 後面多客戶端共用 IP 的熱點。

Sticky session 的代價要算清楚。綁定會讓負載不均——熱門會話集中在某幾台,演算法沒有重新平衡的空間。更重要的是失效轉移成本:被綁定的實例掛了,它記憶體裡的會話狀態就沒了,那些使用者的會話中斷、要重新開始。採用黏著的前提是先界定會話狀態落在哪、以及那台掛掉時的回復路徑。這條代價指向一個更根本的設計選擇:把會話狀態外置、讓實例保持無狀態,任何實例都能接任何請求,就不必靠黏著把狀態鎖在某一台本地。這是 水平擴展的前提 要展開的主題。

選型收斂

演算法的選擇收斂到三個問題。請求成本均勻、實例同質 → round-robin,最簡單。實例不同質 → 加權。請求成本差異大 → least-connections,看當前負載。需要會話或快取親和性 → 雜湊:不在意擴縮容時的重新映射用 IP hash,在意(有快取要保溫、實例常增減)用 consistent hash。需要會話層的黏著且能承受它的失效成本 → sticky session,但先評估把狀態外置是不是更好的解。判準始終是流量的兩個性質:請求成本均不均、需不需要親和性。

下一步路由