<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Consistent-Hash on Tarragon</title><link>https://tarrragon.github.io/blog/tags/consistent-hash/</link><description>Recent content in Consistent-Hash on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 03 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/consistent-hash/index.xml" rel="self" type="application/rss+xml"/><item><title>負載分散演算法</title><link>https://tarrragon.github.io/blog/devops/01-load-balancing/load-balancing-algorithms/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/01-load-balancing/load-balancing-algorithms/</guid><description>&lt;p>下一個請求該送給後端群組裡的哪個實例？負載分散演算法就是在回答這件事，選擇沿兩個維度展開——這個演算法看不看實例當前的負載狀態、以及它需不需要把同一來源固定綁到同一實例（親和性）。這兩個維度決定了一個演算法在什麼流量型態下分得均勻、什麼型態下會製造熱點。&lt;/p>
&lt;p>每個演算法各適合一種流量型態，沒有一個在所有情境都最好。均勻同質的請求適合最簡單的輪流，成本差異大的請求需要看當前負載，有快取或會話親和性需求的則需要雜湊。選錯的代價是負載不均：一部分實例過載、一部分閒置，整體容量被最忙的那台限制住。&lt;/p>
&lt;h2 id="round-robin假設同質均勻的輪流">Round-robin：假設同質均勻的輪流&lt;/h2>
&lt;p>Round-robin 依序把請求輪流分給每個實例，不看任何實例的當前狀態。它成立的前提是兩個同質假設：後端實例規格一致、每個請求的處理成本相近。這兩個假設成立時，輪流就能讓負載自然均勻，且實作最簡單、沒有需要維護的狀態。&lt;/p>
&lt;p>實例規格不一致時，用加權輪流（weighted round-robin）補——規格大的實例配高權重，分到的請求比例對應它的處理能力。加權處理的是「實例不同質」，但它仍不看請求成本：每個請求被當成等重來分。所以當請求成本差異大（有的請求 10 毫秒回、有的要跑 5 秒的查詢），輪流不管加不加權都會失準——一台實例連續接到幾個重請求就過載了，但輪流還是按順序繼續往它送。&lt;/p>
&lt;h2 id="least-connections看當前負載的分配">Least-connections：看當前負載的分配&lt;/h2>
&lt;p>Least-connections 把請求送給當前連線數最少的實例，用「連線數」當實例忙碌程度的近似。它適合請求成本差異大的流量：重請求會讓一台實例的連線數維持得高，演算法就自動少送新請求給它，把流量導向連線數低（比較閒）的實例。輪流看不到的當前負載，least-connections 用連線數看到了。&lt;/p>
&lt;p>連線數是負載的近似、不是負載本身。長連線場景（WebSocket、SSE）下，一個連線數不代表一個當前工作量——一個閒置的長連線也算一條連線。這時連線數會高估閒置連線多的實例的負載。要更準的話得看實際的處理指標（in-flight 請求數、CPU），但那需要實例回報更多狀態，成本更高。連線數是「便宜且多數時候夠用」的折衷。&lt;/p>
&lt;h2 id="看負載的兩個變體p2c-與-least-time">看負載的兩個變體：P2C 與 least-time&lt;/h2>
&lt;p>least-connections 在大規模或分散式的負載平衡上有個弱點：它要維護所有實例的全域連線數狀態，跨多個 LB 節點時這個全域狀態很貴、也容易不同步，還會讓每個 LB 都往「當前最閒的那一台」擠、造成羊群效應。power-of-two-choices（P2C）用一個便宜的近似解掉這個問題——隨機挑兩台、把請求送給其中連線數較少的那台。它幾乎不需要全域狀態（只比兩台），效果卻接近完整的 least-connections，同時避開了羊群效應。這是 Envoy 與多數 service mesh 的預設演算法。&lt;/p>
&lt;p>least-response-time（least-time）則換一個負載近似：用實例的回應時間、而不是連線數。一台連線數低、但每個請求都跑得慢的實例，least-connections 會繼續送、least-time 會避開它——因為它直接以延遲衡量「這台現在扛得動嗎」。nginx Plus、HAProxy 都支援，代價是要持續量測各實例的回應時間。連線數、in-flight 請求、回應時間，是同一條光譜上由便宜到精準的三個負載近似。&lt;/p>
&lt;h2 id="hash-based需要親和性時的固定映射">Hash-based：需要親和性時的固定映射&lt;/h2>
&lt;p>當同一來源的請求需要固定落到同一實例時，用雜湊。IP hash 依 client IP 算雜湊、固定分到某個實例，讓同一個客戶端的請求總是打到同一台。它提供一種不需要外部儲存的會話親和性——會話狀態存在那台實例的記憶體裡，靠 IP hash 保證後續請求回到同一台。&lt;/p>
&lt;p>IP hash 有兩個要注意的失準點。一是分佈不均：大量客戶端藏在同一個 NAT 或 proxy 後面時，它們對外是同一個 IP，會被雜湊到同一台實例，造成熱點。二是後端增減時的大規模重新映射——實例數量一變，雜湊的模數變了，大部分 IP 的落點都會改變，記憶體裡的會話狀態集體失效。&lt;/p>
&lt;p>Consistent hash 解的是第二個問題。它把實例排在一個雜湊環上，一個 key 落到環上順時針的下一個實例；增減一台實例時，只有環上相鄰的一小段 key 需要重新映射，其餘不動。這讓它適合有快取親和性的場景——同一個 key 固定打到同一台、命中那台的本地快取，且擴縮容時不會讓整片快取失效。代價是實作比取模雜湊複雜，且要用虛擬節點來讓 key 在環上分佈得夠均勻。&lt;/p>
&lt;h2 id="sticky-session親和性的會話層實作">Sticky session：親和性的會話層實作&lt;/h2>
&lt;p>Sticky session（會話黏著）是親和性的另一種實作，把同一個會話綁定到同一實例，常見做法是 LB 發一個 cookie 標記該會話該去哪台。它跟 IP hash 解同一類問題（會話狀態留在實例本地、需要後續請求回到同一台），但綁定的粒度是會話而非 IP，避開了 NAT 後面多客戶端共用 IP 的熱點。&lt;/p>
&lt;p>Sticky session 的代價要算清楚。綁定會讓負載不均——熱門會話集中在某幾台，演算法沒有重新平衡的空間。更重要的是失效轉移成本：被綁定的實例掛了，它記憶體裡的會話狀態就沒了，那些使用者的會話中斷、要重新開始。採用黏著的前提是先界定會話狀態落在哪、以及那台掛掉時的回復路徑。這條代價指向一個更根本的設計選擇：把會話狀態外置、讓實例保持無狀態，任何實例都能接任何請求，就不必靠黏著把狀態鎖在某一台本地。這是 &lt;a href="https://tarrragon.github.io/blog/devops/01-load-balancing/scaling-prerequisite/" data-link-title="LB 是水平擴展的前提" data-link-desc="想靠加實例分攤流量卻發現擴不動時，釐清水平擴展的兩個前提——流量要分得進新實例（LB）、任何實例要能接任何請求（無狀態）">水平擴展的前提&lt;/a> 要展開的主題。&lt;/p>
&lt;h2 id="選型收斂">選型收斂&lt;/h2>
&lt;p>演算法的選擇收斂到三個問題。請求成本均勻、實例同質 → round-robin，最簡單。實例不同質 → 加權。請求成本差異大 → least-connections，看當前負載。需要會話或快取親和性 → 雜湊：不在意擴縮容時的重新映射用 IP hash，在意（有快取要保溫、實例常增減）用 consistent hash。需要會話層的黏著且能承受它的失效成本 → sticky session，但先評估把狀態外置是不是更好的解。判準始終是流量的兩個性質：請求成本均不均、需不需要親和性。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>反向代理怎麼把這些演算法接進整體職責 → &lt;a href="https://tarrragon.github.io/blog/devops/01-load-balancing/reverse-proxy-responsibilities/" data-link-title="反向代理的職責" data-link-desc="釐清反向代理在單一入口後面承擔哪些職責、TLS 終止與路由怎麼設計、以及一條請求路徑上的 timeout 為什麼要由外到內遞減時回來讀">反向代理的職責&lt;/a>&lt;/li>
&lt;li>在 nginx 上怎麼配 round-robin、least_conn、ip_hash → &lt;a href="https://tarrragon.github.io/blog/devops/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置&lt;/a>&lt;/li>
&lt;li>把會話狀態外置、讓實例無狀態，是黏著的替代解 → &lt;a href="https://tarrragon.github.io/blog/devops/01-load-balancing/scaling-prerequisite/" data-link-title="LB 是水平擴展的前提" data-link-desc="想靠加實例分攤流量卻發現擴不動時，釐清水平擴展的兩個前提——流量要分得進新實例（LB）、任何實例要能接任何請求（無狀態）">LB 是水平擴展的前提&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>下一個請求該送給後端群組裡的哪個實例？負載分散演算法就是在回答這件事，選擇沿兩個維度展開——這個演算法看不看實例當前的負載狀態、以及它需不需要把同一來源固定綁到同一實例（親和性）。這兩個維度決定了一個演算法在什麼流量型態下分得均勻、什麼型態下會製造熱點。</p>
<p>每個演算法各適合一種流量型態，沒有一個在所有情境都最好。均勻同質的請求適合最簡單的輪流，成本差異大的請求需要看當前負載，有快取或會話親和性需求的則需要雜湊。選錯的代價是負載不均：一部分實例過載、一部分閒置，整體容量被最忙的那台限制住。</p>
<h2 id="round-robin假設同質均勻的輪流">Round-robin：假設同質均勻的輪流</h2>
<p>Round-robin 依序把請求輪流分給每個實例，不看任何實例的當前狀態。它成立的前提是兩個同質假設：後端實例規格一致、每個請求的處理成本相近。這兩個假設成立時，輪流就能讓負載自然均勻，且實作最簡單、沒有需要維護的狀態。</p>
<p>實例規格不一致時，用加權輪流（weighted round-robin）補——規格大的實例配高權重，分到的請求比例對應它的處理能力。加權處理的是「實例不同質」，但它仍不看請求成本：每個請求被當成等重來分。所以當請求成本差異大（有的請求 10 毫秒回、有的要跑 5 秒的查詢），輪流不管加不加權都會失準——一台實例連續接到幾個重請求就過載了，但輪流還是按順序繼續往它送。</p>
<h2 id="least-connections看當前負載的分配">Least-connections：看當前負載的分配</h2>
<p>Least-connections 把請求送給當前連線數最少的實例，用「連線數」當實例忙碌程度的近似。它適合請求成本差異大的流量：重請求會讓一台實例的連線數維持得高，演算法就自動少送新請求給它，把流量導向連線數低（比較閒）的實例。輪流看不到的當前負載，least-connections 用連線數看到了。</p>
<p>連線數是負載的近似、不是負載本身。長連線場景（WebSocket、SSE）下，一個連線數不代表一個當前工作量——一個閒置的長連線也算一條連線。這時連線數會高估閒置連線多的實例的負載。要更準的話得看實際的處理指標（in-flight 請求數、CPU），但那需要實例回報更多狀態，成本更高。連線數是「便宜且多數時候夠用」的折衷。</p>
<h2 id="看負載的兩個變體p2c-與-least-time">看負載的兩個變體：P2C 與 least-time</h2>
<p>least-connections 在大規模或分散式的負載平衡上有個弱點：它要維護所有實例的全域連線數狀態，跨多個 LB 節點時這個全域狀態很貴、也容易不同步，還會讓每個 LB 都往「當前最閒的那一台」擠、造成羊群效應。power-of-two-choices（P2C）用一個便宜的近似解掉這個問題——隨機挑兩台、把請求送給其中連線數較少的那台。它幾乎不需要全域狀態（只比兩台），效果卻接近完整的 least-connections，同時避開了羊群效應。這是 Envoy 與多數 service mesh 的預設演算法。</p>
<p>least-response-time（least-time）則換一個負載近似：用實例的回應時間、而不是連線數。一台連線數低、但每個請求都跑得慢的實例，least-connections 會繼續送、least-time 會避開它——因為它直接以延遲衡量「這台現在扛得動嗎」。nginx Plus、HAProxy 都支援，代價是要持續量測各實例的回應時間。連線數、in-flight 請求、回應時間，是同一條光譜上由便宜到精準的三個負載近似。</p>
<h2 id="hash-based需要親和性時的固定映射">Hash-based：需要親和性時的固定映射</h2>
<p>當同一來源的請求需要固定落到同一實例時，用雜湊。IP hash 依 client IP 算雜湊、固定分到某個實例，讓同一個客戶端的請求總是打到同一台。它提供一種不需要外部儲存的會話親和性——會話狀態存在那台實例的記憶體裡，靠 IP hash 保證後續請求回到同一台。</p>
<p>IP hash 有兩個要注意的失準點。一是分佈不均：大量客戶端藏在同一個 NAT 或 proxy 後面時，它們對外是同一個 IP，會被雜湊到同一台實例，造成熱點。二是後端增減時的大規模重新映射——實例數量一變，雜湊的模數變了，大部分 IP 的落點都會改變，記憶體裡的會話狀態集體失效。</p>
<p>Consistent hash 解的是第二個問題。它把實例排在一個雜湊環上，一個 key 落到環上順時針的下一個實例；增減一台實例時，只有環上相鄰的一小段 key 需要重新映射，其餘不動。這讓它適合有快取親和性的場景——同一個 key 固定打到同一台、命中那台的本地快取，且擴縮容時不會讓整片快取失效。代價是實作比取模雜湊複雜，且要用虛擬節點來讓 key 在環上分佈得夠均勻。</p>
<h2 id="sticky-session親和性的會話層實作">Sticky session：親和性的會話層實作</h2>
<p>Sticky session（會話黏著）是親和性的另一種實作，把同一個會話綁定到同一實例，常見做法是 LB 發一個 cookie 標記該會話該去哪台。它跟 IP hash 解同一類問題（會話狀態留在實例本地、需要後續請求回到同一台），但綁定的粒度是會話而非 IP，避開了 NAT 後面多客戶端共用 IP 的熱點。</p>
<p>Sticky session 的代價要算清楚。綁定會讓負載不均——熱門會話集中在某幾台，演算法沒有重新平衡的空間。更重要的是失效轉移成本：被綁定的實例掛了，它記憶體裡的會話狀態就沒了，那些使用者的會話中斷、要重新開始。採用黏著的前提是先界定會話狀態落在哪、以及那台掛掉時的回復路徑。這條代價指向一個更根本的設計選擇：把會話狀態外置、讓實例保持無狀態，任何實例都能接任何請求，就不必靠黏著把狀態鎖在某一台本地。這是 <a href="/blog/devops/01-load-balancing/scaling-prerequisite/" data-link-title="LB 是水平擴展的前提" data-link-desc="想靠加實例分攤流量卻發現擴不動時，釐清水平擴展的兩個前提——流量要分得進新實例（LB）、任何實例要能接任何請求（無狀態）">水平擴展的前提</a> 要展開的主題。</p>
<h2 id="選型收斂">選型收斂</h2>
<p>演算法的選擇收斂到三個問題。請求成本均勻、實例同質 → round-robin，最簡單。實例不同質 → 加權。請求成本差異大 → least-connections，看當前負載。需要會話或快取親和性 → 雜湊：不在意擴縮容時的重新映射用 IP hash，在意（有快取要保溫、實例常增減）用 consistent hash。需要會話層的黏著且能承受它的失效成本 → sticky session，但先評估把狀態外置是不是更好的解。判準始終是流量的兩個性質：請求成本均不均、需不需要親和性。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>反向代理怎麼把這些演算法接進整體職責 → <a href="/blog/devops/01-load-balancing/reverse-proxy-responsibilities/" data-link-title="反向代理的職責" data-link-desc="釐清反向代理在單一入口後面承擔哪些職責、TLS 終止與路由怎麼設計、以及一條請求路徑上的 timeout 為什麼要由外到內遞減時回來讀">反向代理的職責</a></li>
<li>在 nginx 上怎麼配 round-robin、least_conn、ip_hash → <a href="/blog/devops/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置</a></li>
<li>把會話狀態外置、讓實例無狀態，是黏著的替代解 → <a href="/blog/devops/01-load-balancing/scaling-prerequisite/" data-link-title="LB 是水平擴展的前提" data-link-desc="想靠加實例分攤流量卻發現擴不動時，釐清水平擴展的兩個前提——流量要分得進新實例（LB）、任何實例要能接任何請求（無狀態）">LB 是水平擴展的前提</a></li>
</ul>
]]></content:encoded></item></channel></rss>