<?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>模組一：負載平衡與反向代理 on Tarragon</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/</link><description>Recent content in 模組一：負載平衡與反向代理 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/operations/01-load-balancing/index.xml" rel="self" type="application/rss+xml"/><item><title>反向代理的職責</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/reverse-proxy-responsibilities/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/01-load-balancing/reverse-proxy-responsibilities/</guid><description>&lt;p>反向代理是擋在使用者與後端服務之間的單一入口：使用者連的永遠是它，它再決定把每個請求送給後面哪個服務實例。這一層存在的理由是把「對外的穩定介面」跟「對內的可變拓撲」分開——後端實例可以增減、替換、搬家，使用者看到的入口位址不變。少了這一層，每個後端實例的位址都會外露，換一台機器就要通知所有客戶端改連線。&lt;/p>
&lt;p>反向代理承擔四類職責：TLS 終止、路由、負載分散、健康檢查。負載分散怎麼選演算法是 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法&lt;/a> 的主題，健康檢查怎麼設計是 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計&lt;/a> 的主題；這一章先把四類職責的邊界劃清楚，並展開 TLS 終止、路由、以及貫穿它們的連線管理。&lt;/p>
&lt;h2 id="tls-終止把加密收在入口">TLS 終止：把加密收在入口&lt;/h2>
&lt;p>TLS 終止指反向代理在入口解開 HTTPS 加密，後端拿到的是解密後的 HTTP。這樣設計把憑證管理跟加解密的成本集中在一個地方——憑證只需要裝在入口這一層，後端實例不必各自持有憑證、不必各自做加解密。要新增一個後端實例時，它不需要知道任何 TLS 的事，入口已經把加密處理掉了。&lt;/p>
&lt;p>集中的代價是入口到後端這段變成明文。這段通常跑在私有網路內（後端住在 private subnet、只接受來自入口的流量），威脅模型是「這段網路是可信的」；若這段也要加密（例如合規要求端到端加密），就要做 TLS 重新加密或 mTLS，那是把成本換回分散。入口該接受哪些 TLS 版本、憑證怎麼簽發與續期，在雲端 LB 上是 IaC 的描述範圍，&lt;a href="https://tarrragon.github.io/blog/infra/05-core-services/loadbalancer-alb/" data-link-title="入口上 IaC — ALB、TLS 與健康檢查" data-link-desc="Application Load Balancer 的 listener、target group、健康檢查閾值設計，以及用 ACM 把 TLS 憑證的簽發、驗證與掛載整條鏈寫進版本控制">infra 的 ALB 篇&lt;/a> 有 &lt;code>ssl_policy&lt;/code>、ACM 簽發與 DNS 驗證的完整 IaC；這一章關注的是「終止在入口」這個職責決策本身。&lt;/p>
&lt;h2 id="路由一個入口分流到多個服務">路由：一個入口分流到多個服務&lt;/h2>
&lt;p>路由指反向代理依請求的特徵（host header、路徑）把流量分到不同的後端群組。這讓多個服務共用一個入口——&lt;code>/auth/*&lt;/code> 導向認證服務、&lt;code>/api/*&lt;/code> 導向 API 服務，各自是獨立的後端群組，但對外只有一個網域、一個入口。路由的收斂價值在成本與管理面：共用一個入口、用路由規則分流，省下每個服務各開一個入口的固定成本。&lt;/p>
&lt;p>分流的邊界要看服務之間的差異。流量特徵、安全等級接近的服務適合共用入口，用路由規則分開就夠；當某個服務需要獨立的防火牆規則、獨立的流量隔離時，替它開獨立入口才合理。路由規則本身是聲明式的（哪個 host 或 path 對到哪個後端群組），複雜的路由邏輯（依 header、依權重、依版本做灰度）會把入口從單純的分流變成流量控制面，那牽涉到部署切換的節奏，屬於 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">backend 的流量配置控制面&lt;/a> 的範圍。&lt;/p>
&lt;h2 id="負載分散與健康檢查這裡先劃邊界">負載分散與健康檢查：這裡先劃邊界&lt;/h2>
&lt;p>負載分散是反向代理把流量分攤到同一個後端群組內多個實例的職責，健康檢查是它判斷某個實例還能不能接流量的職責。這兩者關係緊密——負載分散只把流量分給健康檢查認為健康的實例。演算法怎麼選（誰接下一個請求）在 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法&lt;/a> 展開，健康怎麼判（被動觀察還是主動探測、多久判一次）在 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計&lt;/a> 展開。這一章只確立它們是反向代理的職責、且互相依賴。&lt;/p>
&lt;h2 id="連線管理timeout-要由外到內遞減">連線管理：timeout 要由外到內遞減&lt;/h2>
&lt;p>反向代理夾在使用者與後端之間，兩側的連線都由它管，其中最容易設錯的是 timeout。一條請求路徑上有多層 timeout——瀏覽器、CDN、反向代理、應用、資料庫，每層各有預設值。設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>典型 timeout 範圍&lt;/th>
 &lt;th>設定位置&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Client / Browser&lt;/td>
 &lt;td>30-120 秒&lt;/td>
 &lt;td>前端 fetch / SDK 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>CDN vendor 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>反向代理 / LB&lt;/td>
 &lt;td>30-60 秒&lt;/td>
 &lt;td>LB idle timeout / request timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application&lt;/td>
 &lt;td>5-30 秒&lt;/td>
 &lt;td>HTTP server read/write timeout&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Database / Cache&lt;/td>
 &lt;td>1-5 秒&lt;/td>
 &lt;td>連線池 query / connect timeout&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>遞減的理由是避免「外層先放棄、內層還在做白工」。如果反向代理 timeout 設 30 秒、應用設 60 秒，代理會在 30 秒回 504 給使用者，但應用仍持有連線等資料庫回應——佔用連線資源卻交付不了結果。這類失誤最常見的版本是只調反向代理這一層：使用者回報 timeout，就把代理的 timeout 從 30 秒調到 120 秒。結果是慢請求佔用連線更久、連線池被慢請求填滿、正常請求也開始排隊。穩定的做法是先在應用或資料庫層找出延遲根因，而不是放大外層 timeout 去「等更久」。&lt;/p>
&lt;h2 id="反向代理是部署與事故的決策點">反向代理是部署與事故的決策點&lt;/h2>
&lt;p>把反向代理當成「只做轉發」的元件，會低估它在部署與事故裡的決策角色。它的設定定義了流量怎麼切換、回退可不可行、故障擴散多快——TLS 收在哪、路由怎麼分、timeout 怎麼串、健康怎麼判，每一項都在正常時無感、在事故時決定損失大小。這一層的完整合約（routing、health、connection、drain 四部分怎麼協同）在 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">backend 的 load balancer 合約&lt;/a> 展開。&lt;/p></description><content:encoded><![CDATA[<p>反向代理是擋在使用者與後端服務之間的單一入口：使用者連的永遠是它，它再決定把每個請求送給後面哪個服務實例。這一層存在的理由是把「對外的穩定介面」跟「對內的可變拓撲」分開——後端實例可以增減、替換、搬家，使用者看到的入口位址不變。少了這一層，每個後端實例的位址都會外露，換一台機器就要通知所有客戶端改連線。</p>
<p>反向代理承擔四類職責：TLS 終止、路由、負載分散、健康檢查。負載分散怎麼選演算法是 <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a> 的主題，健康檢查怎麼設計是 <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a> 的主題；這一章先把四類職責的邊界劃清楚，並展開 TLS 終止、路由、以及貫穿它們的連線管理。</p>
<h2 id="tls-終止把加密收在入口">TLS 終止：把加密收在入口</h2>
<p>TLS 終止指反向代理在入口解開 HTTPS 加密，後端拿到的是解密後的 HTTP。這樣設計把憑證管理跟加解密的成本集中在一個地方——憑證只需要裝在入口這一層，後端實例不必各自持有憑證、不必各自做加解密。要新增一個後端實例時，它不需要知道任何 TLS 的事，入口已經把加密處理掉了。</p>
<p>集中的代價是入口到後端這段變成明文。這段通常跑在私有網路內（後端住在 private subnet、只接受來自入口的流量），威脅模型是「這段網路是可信的」；若這段也要加密（例如合規要求端到端加密），就要做 TLS 重新加密或 mTLS，那是把成本換回分散。入口該接受哪些 TLS 版本、憑證怎麼簽發與續期，在雲端 LB 上是 IaC 的描述範圍，<a href="/blog/infra/05-core-services/loadbalancer-alb/" data-link-title="入口上 IaC — ALB、TLS 與健康檢查" data-link-desc="Application Load Balancer 的 listener、target group、健康檢查閾值設計，以及用 ACM 把 TLS 憑證的簽發、驗證與掛載整條鏈寫進版本控制">infra 的 ALB 篇</a> 有 <code>ssl_policy</code>、ACM 簽發與 DNS 驗證的完整 IaC；這一章關注的是「終止在入口」這個職責決策本身。</p>
<h2 id="路由一個入口分流到多個服務">路由：一個入口分流到多個服務</h2>
<p>路由指反向代理依請求的特徵（host header、路徑）把流量分到不同的後端群組。這讓多個服務共用一個入口——<code>/auth/*</code> 導向認證服務、<code>/api/*</code> 導向 API 服務，各自是獨立的後端群組，但對外只有一個網域、一個入口。路由的收斂價值在成本與管理面：共用一個入口、用路由規則分流，省下每個服務各開一個入口的固定成本。</p>
<p>分流的邊界要看服務之間的差異。流量特徵、安全等級接近的服務適合共用入口，用路由規則分開就夠；當某個服務需要獨立的防火牆規則、獨立的流量隔離時，替它開獨立入口才合理。路由規則本身是聲明式的（哪個 host 或 path 對到哪個後端群組），複雜的路由邏輯（依 header、依權重、依版本做灰度）會把入口從單純的分流變成流量控制面，那牽涉到部署切換的節奏，屬於 <a href="/blog/backend/05-deployment-platform/traffic-config-control-plane-boundary/" data-link-title="5.7 Traffic、Config 與 Control Plane Boundary" data-link-desc="說明流量、設定、secret、service discovery 與管理面如何分責任與回退。">backend 的流量配置控制面</a> 的範圍。</p>
<h2 id="負載分散與健康檢查這裡先劃邊界">負載分散與健康檢查：這裡先劃邊界</h2>
<p>負載分散是反向代理把流量分攤到同一個後端群組內多個實例的職責，健康檢查是它判斷某個實例還能不能接流量的職責。這兩者關係緊密——負載分散只把流量分給健康檢查認為健康的實例。演算法怎麼選（誰接下一個請求）在 <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a> 展開，健康怎麼判（被動觀察還是主動探測、多久判一次）在 <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a> 展開。這一章只確立它們是反向代理的職責、且互相依賴。</p>
<h2 id="連線管理timeout-要由外到內遞減">連線管理：timeout 要由外到內遞減</h2>
<p>反向代理夾在使用者與後端之間，兩側的連線都由它管，其中最容易設錯的是 timeout。一條請求路徑上有多層 timeout——瀏覽器、CDN、反向代理、應用、資料庫，每層各有預設值。設計原則是由外到內遞減：外層（離使用者近）的 timeout 要大於內層（離資料源近）。</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>典型 timeout 範圍</th>
          <th>設定位置</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Client / Browser</td>
          <td>30-120 秒</td>
          <td>前端 fetch / SDK 設定</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>5-30 秒</td>
          <td>CDN vendor 設定</td>
      </tr>
      <tr>
          <td>反向代理 / LB</td>
          <td>30-60 秒</td>
          <td>LB idle timeout / request timeout</td>
      </tr>
      <tr>
          <td>Application</td>
          <td>5-30 秒</td>
          <td>HTTP server read/write timeout</td>
      </tr>
      <tr>
          <td>Database / Cache</td>
          <td>1-5 秒</td>
          <td>連線池 query / connect timeout</td>
      </tr>
  </tbody>
</table>
<p>遞減的理由是避免「外層先放棄、內層還在做白工」。如果反向代理 timeout 設 30 秒、應用設 60 秒，代理會在 30 秒回 504 給使用者，但應用仍持有連線等資料庫回應——佔用連線資源卻交付不了結果。這類失誤最常見的版本是只調反向代理這一層：使用者回報 timeout，就把代理的 timeout 從 30 秒調到 120 秒。結果是慢請求佔用連線更久、連線池被慢請求填滿、正常請求也開始排隊。穩定的做法是先在應用或資料庫層找出延遲根因，而不是放大外層 timeout 去「等更久」。</p>
<h2 id="反向代理是部署與事故的決策點">反向代理是部署與事故的決策點</h2>
<p>把反向代理當成「只做轉發」的元件，會低估它在部署與事故裡的決策角色。它的設定定義了流量怎麼切換、回退可不可行、故障擴散多快——TLS 收在哪、路由怎麼分、timeout 怎麼串、健康怎麼判，每一項都在正常時無感、在事故時決定損失大小。這一層的完整合約（routing、health、connection、drain 四部分怎麼協同）在 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">backend 的 load balancer 合約</a> 展開。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>入口層的整體地圖（DNS → 負載平衡 → reverse proxy → 應用的責任鏈、TLS 終結位置、單機與雲端入口的選擇條件）→ <a href="/blog/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">infra：流量入口層</a></li>
<li>流量分給哪個實例、依什麼演算法 → <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a></li>
<li>在 nginx 上把 upstream、健康檢查、timeout 配起來 → <a href="/blog/operations/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置</a></li>
<li>健康怎麼判、多久判一次、判錯的後果 → <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a></li>
<li>反向代理的 IaC 描述（listener、target group、TLS、健康檢查）→ <a href="/blog/infra/05-core-services/loadbalancer-alb/" data-link-title="入口上 IaC — ALB、TLS 與健康檢查" data-link-desc="Application Load Balancer 的 listener、target group、健康檢查閾值設計，以及用 ACM 把 TLS 憑證的簽發、驗證與掛載整條鏈寫進版本控制">infra：入口上 IaC</a></li>
</ul>
]]></content:encoded></item><item><title>負載分散演算法</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/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/operations/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>入口層的整體地圖（DNS → 負載平衡 → reverse proxy → 應用的責任鏈、TLS 終結位置、單機與雲端入口的選擇條件）→ &lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">infra：流量入口層&lt;/a>&lt;/li>
&lt;li>反向代理怎麼把這些演算法接進整體職責 → &lt;a href="https://tarrragon.github.io/blog/operations/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/operations/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/operations/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/operations/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>入口層的整體地圖（DNS → 負載平衡 → reverse proxy → 應用的責任鏈、TLS 終結位置、單機與雲端入口的選擇條件）→ <a href="/blog/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">infra：流量入口層</a></li>
<li>反向代理怎麼把這些演算法接進整體職責 → <a href="/blog/operations/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/operations/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置</a></li>
<li>把會話狀態外置、讓實例無狀態，是黏著的替代解 → <a href="/blog/operations/01-load-balancing/scaling-prerequisite/" data-link-title="LB 是水平擴展的前提" data-link-desc="想靠加實例分攤流量卻發現擴不動時，釐清水平擴展的兩個前提——流量要分得進新實例（LB）、任何實例要能接任何請求（無狀態）">LB 是水平擴展的前提</a></li>
</ul>
]]></content:encoded></item><item><title>nginx 實務配置</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/nginx-configuration/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/01-load-balancing/nginx-configuration/</guid><description>&lt;p>nginx 當反向代理的核心是一個 &lt;code>upstream&lt;/code> 區塊加 &lt;code>proxy_pass&lt;/code>：&lt;code>upstream&lt;/code> 定義後端群組跟分散策略，&lt;code>proxy_pass&lt;/code> 把請求轉過去。最小配置很短，但真正決定線上行為的是三個判讀點——用哪種負載分散方法、健康檢查怎麼配、timeout 怎麼串。其中開源 nginx 有一個最容易踩的限制：主動健康檢查是商業版功能，這一點不知道會讓人以為配了主動探測、實際上根本沒生效。&lt;/p>
&lt;p>以下配置在 nginx 1.30.3（stable）上以 &lt;code>nginx -t&lt;/code> 驗證過語法。&lt;/p>
&lt;h2 id="upstream-與負載分散方法">upstream 與負載分散方法&lt;/h2>
&lt;p>&lt;code>upstream&lt;/code> 區塊列出後端伺服器，並用一個指令選負載分散方法。預設是 round-robin（不寫任何方法指令就是它）；&lt;code>least_conn&lt;/code> 送給當前連線數最少的後端；&lt;code>ip_hash&lt;/code> 依 client IP 固定分配；&lt;code>hash $key consistent&lt;/code> 是加了 &lt;code>consistent&lt;/code> 參數的一致性雜湊。這些方法對應 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法&lt;/a> 講的選擇，nginx 只是把它們寫成一行指令。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">upstream&lt;/span> &lt;span class="s">api&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kn">least_conn&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kn">server&lt;/span> &lt;span class="n">10.0.1.11&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8080&lt;/span> &lt;span class="s">max_fails=3&lt;/span> &lt;span class="s">fail_timeout=15s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kn">server&lt;/span> &lt;span class="n">10.0.1.12&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8080&lt;/span> &lt;span class="s">max_fails=3&lt;/span> &lt;span class="s">fail_timeout=15s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="kn">server&lt;/span> &lt;span class="n">10.0.1.13&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="mi">8080&lt;/span> &lt;span class="s">backup&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kn">keepalive&lt;/span> &lt;span class="mi">32&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>server&lt;/code> 行上的參數各有用途。&lt;code>backup&lt;/code> 標記的後端平時不接流量，只在其他後端都不可用時才頂上——當降級備援用。&lt;code>keepalive 32&lt;/code> 讓 nginx 對後端維持一個連線池、重用連線，省下每個請求都重新握手的成本；高流量下這個設定對延遲影響明顯。&lt;code>weight=N&lt;/code>（未在上例）給規格不同的後端配權重，對應加權輪流。&lt;/p>
&lt;h2 id="被動健康檢查開源版的健康檢查">被動健康檢查：開源版的健康檢查&lt;/h2>
&lt;p>開源 nginx 的健康檢查是被動的，靠 &lt;code>server&lt;/code> 行上的 &lt;code>max_fails&lt;/code> 跟 &lt;code>fail_timeout&lt;/code> 兩個參數。語意是：在 &lt;code>fail_timeout&lt;/code> 這段時間內，轉發給某個後端的請求失敗達到 &lt;code>max_fails&lt;/code> 次，nginx 就把這個後端標記為不可用、停送 &lt;code>fail_timeout&lt;/code> 秒，之後再試探性地放流量回去。上例的 &lt;code>max_fails=3 fail_timeout=15s&lt;/code> 表示 15 秒內失敗 3 次就暫停這個後端 15 秒。&lt;/p>
&lt;p>被動探測的機制是從轉發的真實流量觀察失敗、不額外發探測請求，&lt;code>max_fails&lt;/code> 就是「觀察到幾次失敗才摘除」的門檻。這種機制的盲點、以及跟主動探測的完整對照，在 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計&lt;/a> 展開，這裡只確立 nginx 開源版用的是被動機制。&lt;/p>
&lt;h2 id="gotcha主動-health_check-是商業版功能">gotcha：主動 health_check 是商業版功能&lt;/h2>
&lt;p>想在 nginx 上配主動健康檢查（定時對後端 &lt;code>/healthz&lt;/code> 探測、不靠真實流量），會發現 &lt;code>health_check&lt;/code> 這個指令在開源版根本不存在。在 nginx 1.30.3 上放一個 &lt;code>health_check&lt;/code> 指令跑 &lt;code>nginx -t&lt;/code>，直接報 &lt;code>[emerg] unknown directive &amp;quot;health_check&amp;quot;&lt;/code>、配置測試失敗——它是 nginx Plus（商業訂閱）才有的指令。這個限制不知道的話，很容易照著某些教學把 &lt;code>health_check&lt;/code> 寫進去、以為配了主動探測，實際上配置根本載入不了，或在別處抄來的 Plus 配置直接讓 nginx 起不來。&lt;/p>
&lt;p>開源版要主動探測有兩條路：一是裝第三方模組（如 &lt;code>nginx_upstream_check_module&lt;/code>），要重新編譯 nginx，維護成本較高；二是不在 nginx 內做，改用外部探測——一個獨立的定時器對後端探測，探到不健康就從服務發現或配置裡摘掉。多數情況下，被動的 &lt;code>max_fails&lt;/code> 加上外部的主動探測，就覆蓋了「後端回應了但一直出錯」跟「後端整個沒回應」兩種失效，不必為了主動探測上商業版。&lt;/p>
&lt;h2 id="proxy-header-陷阱後端看到的來源-ip">proxy header 陷阱：後端看到的來源 IP&lt;/h2>
&lt;p>&lt;code>proxy_pass&lt;/code> 轉發時，如果不主動設 header，後端看到的來源 IP 會是 nginx 的 IP、而不是真實客戶端的——因為連線是 nginx 發起的。這會讓後端的存取日誌、限流、地理判斷全部失準，全世界的請求看起來都來自 nginx 那台。修法是明確把原始資訊放進 header：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-nginx" data-lang="nginx">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">location&lt;/span> &lt;span class="s">/&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_pass&lt;/span> &lt;span class="s">http://api&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_set_header&lt;/span> &lt;span class="s">Host&lt;/span> &lt;span class="nv">$host&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_set_header&lt;/span> &lt;span class="s">X-Forwarded-For&lt;/span> &lt;span class="nv">$proxy_add_x_forwarded_for&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_connect_timeout&lt;/span> &lt;span class="s">5s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_read_timeout&lt;/span> &lt;span class="s">30s&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> &lt;span class="kn">proxy_next_upstream&lt;/span> &lt;span class="s">error&lt;/span> &lt;span class="s">timeout&lt;/span> &lt;span class="s">http_502&lt;/span> &lt;span class="s">http_503&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>proxy_set_header Host $host&lt;/code> 保留原始的 host header（否則後端收到的 Host 會是 upstream 名稱，依 host 分辨虛擬主機的後端會錯亂）。&lt;code>X-Forwarded-For $proxy_add_x_forwarded_for&lt;/code> 把真實客戶端 IP 附進去、且保留鏈上前面的代理紀錄，後端要讀真實 IP 就從這個 header 取。&lt;/p>
&lt;h2 id="timeout-與重試">timeout 與重試&lt;/h2>
&lt;p>&lt;code>proxy_connect_timeout&lt;/code> 是 nginx 連到後端的逾時、&lt;code>proxy_read_timeout&lt;/code> 是等後端回應的逾時。這兩個要放進 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/reverse-proxy-responsibilities/" data-link-title="反向代理的職責" data-link-desc="釐清反向代理在單一入口後面承擔哪些職責、TLS 終止與路由怎麼設計、以及一條請求路徑上的 timeout 為什麼要由外到內遞減時回來讀">反向代理職責&lt;/a> 講的 timeout 層級串聯裡看——nginx 這層的 timeout 要大於它後面的應用層、小於它前面的客戶端層，否則會出現外層先放棄、內層還在做白工。&lt;/p>
&lt;p>&lt;code>proxy_next_upstream&lt;/code> 決定一個後端失敗時要不要把請求重試到下一個後端。上例的 &lt;code>error timeout http_502 http_503&lt;/code> 表示連線錯誤、逾時、後端回 502/503 時自動重試下一台。這個機制對讀請求很有用（一台壞了自動換一台），但對非冪等的寫請求要謹慎——如果一個 POST 已經被後端處理了、只是回應在傳輸中逾時，重試會讓這個 POST 執行第二次。要重試非冪等請求前，得先確認後端有冪等保護（例如靠 idempotency key 去重），否則把 &lt;code>POST&lt;/code> 放進 &lt;code>proxy_next_upstream&lt;/code> 的重試條件會製造重複寫入。&lt;/p></description><content:encoded><![CDATA[<p>nginx 當反向代理的核心是一個 <code>upstream</code> 區塊加 <code>proxy_pass</code>：<code>upstream</code> 定義後端群組跟分散策略，<code>proxy_pass</code> 把請求轉過去。最小配置很短，但真正決定線上行為的是三個判讀點——用哪種負載分散方法、健康檢查怎麼配、timeout 怎麼串。其中開源 nginx 有一個最容易踩的限制：主動健康檢查是商業版功能，這一點不知道會讓人以為配了主動探測、實際上根本沒生效。</p>
<p>以下配置在 nginx 1.30.3（stable）上以 <code>nginx -t</code> 驗證過語法。</p>
<h2 id="upstream-與負載分散方法">upstream 與負載分散方法</h2>
<p><code>upstream</code> 區塊列出後端伺服器，並用一個指令選負載分散方法。預設是 round-robin（不寫任何方法指令就是它）；<code>least_conn</code> 送給當前連線數最少的後端；<code>ip_hash</code> 依 client IP 固定分配；<code>hash $key consistent</code> 是加了 <code>consistent</code> 參數的一致性雜湊。這些方法對應 <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a> 講的選擇，nginx 只是把它們寫成一行指令。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">upstream</span> <span class="s">api</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kn">least_conn</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kn">server</span> <span class="n">10.0.1.11</span><span class="p">:</span><span class="mi">8080</span> <span class="s">max_fails=3</span> <span class="s">fail_timeout=15s</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="kn">server</span> <span class="n">10.0.1.12</span><span class="p">:</span><span class="mi">8080</span> <span class="s">max_fails=3</span> <span class="s">fail_timeout=15s</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kn">server</span> <span class="n">10.0.1.13</span><span class="p">:</span><span class="mi">8080</span> <span class="s">backup</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="kn">keepalive</span> <span class="mi">32</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>server</code> 行上的參數各有用途。<code>backup</code> 標記的後端平時不接流量，只在其他後端都不可用時才頂上——當降級備援用。<code>keepalive 32</code> 讓 nginx 對後端維持一個連線池、重用連線，省下每個請求都重新握手的成本；高流量下這個設定對延遲影響明顯。<code>weight=N</code>（未在上例）給規格不同的後端配權重，對應加權輪流。</p>
<h2 id="被動健康檢查開源版的健康檢查">被動健康檢查：開源版的健康檢查</h2>
<p>開源 nginx 的健康檢查是被動的，靠 <code>server</code> 行上的 <code>max_fails</code> 跟 <code>fail_timeout</code> 兩個參數。語意是：在 <code>fail_timeout</code> 這段時間內，轉發給某個後端的請求失敗達到 <code>max_fails</code> 次，nginx 就把這個後端標記為不可用、停送 <code>fail_timeout</code> 秒，之後再試探性地放流量回去。上例的 <code>max_fails=3 fail_timeout=15s</code> 表示 15 秒內失敗 3 次就暫停這個後端 15 秒。</p>
<p>被動探測的機制是從轉發的真實流量觀察失敗、不額外發探測請求，<code>max_fails</code> 就是「觀察到幾次失敗才摘除」的門檻。這種機制的盲點、以及跟主動探測的完整對照，在 <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a> 展開，這裡只確立 nginx 開源版用的是被動機制。</p>
<h2 id="gotcha主動-health_check-是商業版功能">gotcha：主動 health_check 是商業版功能</h2>
<p>想在 nginx 上配主動健康檢查（定時對後端 <code>/healthz</code> 探測、不靠真實流量），會發現 <code>health_check</code> 這個指令在開源版根本不存在。在 nginx 1.30.3 上放一個 <code>health_check</code> 指令跑 <code>nginx -t</code>，直接報 <code>[emerg] unknown directive &quot;health_check&quot;</code>、配置測試失敗——它是 nginx Plus（商業訂閱）才有的指令。這個限制不知道的話，很容易照著某些教學把 <code>health_check</code> 寫進去、以為配了主動探測，實際上配置根本載入不了，或在別處抄來的 Plus 配置直接讓 nginx 起不來。</p>
<p>開源版要主動探測有兩條路：一是裝第三方模組（如 <code>nginx_upstream_check_module</code>），要重新編譯 nginx，維護成本較高；二是不在 nginx 內做，改用外部探測——一個獨立的定時器對後端探測，探到不健康就從服務發現或配置裡摘掉。多數情況下，被動的 <code>max_fails</code> 加上外部的主動探測，就覆蓋了「後端回應了但一直出錯」跟「後端整個沒回應」兩種失效，不必為了主動探測上商業版。</p>
<h2 id="proxy-header-陷阱後端看到的來源-ip">proxy header 陷阱：後端看到的來源 IP</h2>
<p><code>proxy_pass</code> 轉發時，如果不主動設 header，後端看到的來源 IP 會是 nginx 的 IP、而不是真實客戶端的——因為連線是 nginx 發起的。這會讓後端的存取日誌、限流、地理判斷全部失準，全世界的請求看起來都來自 nginx 那台。修法是明確把原始資訊放進 header：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="kn">proxy_pass</span> <span class="s">http://api</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="kn">proxy_set_header</span> <span class="s">X-Forwarded-For</span> <span class="nv">$proxy_add_x_forwarded_for</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kn">proxy_connect_timeout</span> <span class="s">5s</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="kn">proxy_read_timeout</span> <span class="s">30s</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="kn">proxy_next_upstream</span> <span class="s">error</span> <span class="s">timeout</span> <span class="s">http_502</span> <span class="s">http_503</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p><code>proxy_set_header Host $host</code> 保留原始的 host header（否則後端收到的 Host 會是 upstream 名稱，依 host 分辨虛擬主機的後端會錯亂）。<code>X-Forwarded-For $proxy_add_x_forwarded_for</code> 把真實客戶端 IP 附進去、且保留鏈上前面的代理紀錄，後端要讀真實 IP 就從這個 header 取。</p>
<h2 id="timeout-與重試">timeout 與重試</h2>
<p><code>proxy_connect_timeout</code> 是 nginx 連到後端的逾時、<code>proxy_read_timeout</code> 是等後端回應的逾時。這兩個要放進 <a href="/blog/operations/01-load-balancing/reverse-proxy-responsibilities/" data-link-title="反向代理的職責" data-link-desc="釐清反向代理在單一入口後面承擔哪些職責、TLS 終止與路由怎麼設計、以及一條請求路徑上的 timeout 為什麼要由外到內遞減時回來讀">反向代理職責</a> 講的 timeout 層級串聯裡看——nginx 這層的 timeout 要大於它後面的應用層、小於它前面的客戶端層，否則會出現外層先放棄、內層還在做白工。</p>
<p><code>proxy_next_upstream</code> 決定一個後端失敗時要不要把請求重試到下一個後端。上例的 <code>error timeout http_502 http_503</code> 表示連線錯誤、逾時、後端回 502/503 時自動重試下一台。這個機制對讀請求很有用（一台壞了自動換一台），但對非冪等的寫請求要謹慎——如果一個 POST 已經被後端處理了、只是回應在傳輸中逾時，重試會讓這個 POST 執行第二次。要重試非冪等請求前，得先確認後端有冪等保護（例如靠 idempotency key 去重），否則把 <code>POST</code> 放進 <code>proxy_next_upstream</code> 的重試條件會製造重複寫入。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>入口層的整體地圖（DNS → 負載平衡 → reverse proxy → 應用的責任鏈、TLS 終結位置、單機與雲端入口的選擇條件）→ <a href="/blog/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">infra：流量入口層</a></li>
<li>這些方法各適合什麼流量型態 → <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a></li>
<li>被動與主動健康檢查的完整對照、閾值怎麼定 → <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a></li>
<li>timeout 為什麼要由外到內遞減 → <a href="/blog/operations/01-load-balancing/reverse-proxy-responsibilities/" data-link-title="反向代理的職責" data-link-desc="釐清反向代理在單一入口後面承擔哪些職責、TLS 終止與路由怎麼設計、以及一條請求路徑上的 timeout 為什麼要由外到內遞減時回來讀">反向代理的職責</a></li>
<li>雲端 LB 上等價的 listener、target group、健康檢查怎麼用 IaC 描述 → <a href="/blog/infra/05-core-services/loadbalancer-alb/" data-link-title="入口上 IaC — ALB、TLS 與健康檢查" data-link-desc="Application Load Balancer 的 listener、target group、健康檢查閾值設計，以及用 ACM 把 TLS 憑證的簽發、驗證與掛載整條鏈寫進版本控制">infra：入口上 IaC</a></li>
</ul>
]]></content:encoded></item><item><title>健康檢查路由設計</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/</guid><description>&lt;p>健康檢查路由決定負載平衡把流量送給哪些後端、不送給哪些。設計沿兩個時間窗口展開：一個新後端要多久才開始接流量、一個壞後端要多久才被移出輪替。兩個窗口都可能設錯，且錯的方向相反——太寬鬆，壞掉的後端留在輪替裡繼續讓部分使用者收到錯誤；太嚴格，部署瞬間還在初始化的新後端就被判死，反覆移出移入、使用者看到間歇性失敗。&lt;/p>
&lt;p>這一章講的是負載平衡端怎麼消費健康訊號（多久探一次、幾次算數、探到不健康怎麼辦）。服務端怎麼提供健康訊號、端點回什麼才算健康、check 要探到多深，是 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">模組四的 health check endpoint 設計&lt;/a>——服務怎麼提供跟 LB 怎麼使用是同一個概念的兩側，這裡不重述服務側。&lt;/p>
&lt;h2 id="被動探測與主動探測">被動探測與主動探測&lt;/h2>
&lt;p>負載平衡判斷後端健康有兩種路徑。主動探測是 LB 定時對後端發一個健康請求（例如每 15 秒 curl 一次 &lt;code>/healthz&lt;/code>），依回應判定健康；被動探測是 LB 觀察它轉發的真實流量、不額外發請求——某個後端連續回幾次錯誤或逾時，就把它標成不健康、暫停送流量。&lt;/p>
&lt;p>兩者的盲點不同。主動探測的盲點是探測請求跟真實請求走的路徑可能不同：&lt;code>/healthz&lt;/code> 通了不代表真實的業務端點通，一個只檢查淺層的探測會漏掉只在真實流量上出現的失敗。被動探測的盲點是它要先犧牲幾個真實請求才發現問題——後端壞掉的當下，是靠幾個真實使用者撞到錯誤，LB 才學到它不健康。成熟的做法常常兩者都用：主動探測抓「後端整個沒回應」，被動探測抓「後端回應了但一直出錯」。雲端 LB（如 ALB）預設走主動探測，開源 nginx 預設走被動探測，主動探測要靠額外機制，這個差別在 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置&lt;/a> 展開。&lt;/p>
&lt;h2 id="閾值定出兩個時間窗口">閾值定出兩個時間窗口&lt;/h2>
&lt;p>主動探測的行為由幾個參數的交互決定，它們算術地定出前面說的兩個窗口。以一組具體的 ALB 設定為例：&lt;code>interval = 15&lt;/code>（每 15 秒探一次）、&lt;code>healthy_threshold = 2&lt;/code>（連續 2 次通過才算健康）、&lt;code>unhealthy_threshold = 3&lt;/code>（連續 3 次失敗才算壞）、&lt;code>timeout = 5&lt;/code>（單次探測 5 秒沒回算失敗）。&lt;/p>
&lt;p>這組參數的意思是：一個新後端要等 &lt;code>2 × 15 = 30&lt;/code> 秒（兩次通過）才開始接流量，一個壞掉的後端要 &lt;code>3 × 15 = 45&lt;/code> 秒（三次失敗）才被移出。每個參數的鬆緊都是雙向取捨：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>參數&lt;/th>
 &lt;th>過小的風險&lt;/th>
 &lt;th>過大的風險&lt;/th>
 &lt;th>起點建議&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>interval&lt;/code>&lt;/td>
 &lt;td>探測本身對後端造成額外負擔&lt;/td>
 &lt;td>壞後端被偵測到的延遲增加&lt;/td>
 &lt;td>15-30 秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>healthy_threshold&lt;/code>&lt;/td>
 &lt;td>還沒完全就緒就接流量&lt;/td>
 &lt;td>部署後等太久才開始分流&lt;/td>
 &lt;td>2-3 次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>unhealthy_threshold&lt;/code>&lt;/td>
 &lt;td>暫時性波動導致健康的後端被移出&lt;/td>
 &lt;td>壞後端繼續收流量太久&lt;/td>
 &lt;td>2-3 次&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>timeout&lt;/code>&lt;/td>
 &lt;td>正常但偏慢的回應被誤判為失敗&lt;/td>
 &lt;td>確實掛了卻要等很久才確認&lt;/td>
 &lt;td>5 秒&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>unhealthy_threshold&lt;/code> 設 1 的問題最典型：一次網路抖動、一個偶發的慢回應，就足以把一個健康的後端踢出去，造成不必要的容量波動。要幾次連續失敗才算數，是在「快速摘除壞後端」跟「容忍暫時性抖動」之間取平衡。&lt;/p>
&lt;p>&lt;code>interval&lt;/code> 的取捨在探測頻率與探測負擔之間：探得太密，健康檢查本身變成後端的額外負載（每台都要固定回應探測）；探得太疏，壞後端要更久才被偵測到。&lt;code>timeout&lt;/code> 則要對齊後端正常回應的時間分佈：設得比正常的慢回應還短，會把偶爾偏慢但其實健康的後端誤判成失敗、把它踢出去；設得太長，一台確實掛了的後端要拖很久才確認。這兩個參數跟兩個 threshold 相乘，才共同定出前面那兩個窗口的實際長度。&lt;/p>
&lt;h2 id="flapping部署瞬間的移出移入">flapping：部署瞬間的移出移入&lt;/h2>
&lt;p>健康檢查最常見的失效表現是 flapping——每次部署時，新後端在健康與不健康之間反覆跳動。根因幾乎都是同一個：應用的啟動時間超過了「開始接流量」的窗口。新容器起來、應用還在初始化（載入索引、建連線池），但健康檢查已經開始探測，探到還沒就緒的應用、判不健康、移出；應用初始化完又通過、移入；下一次探測可能又撞上某個還沒好的部分。&lt;/p>
&lt;p>判讀方式是觀察後端在健康與不健康之間的轉換次數。每次部署都看到新後端跳動，代表初始等待不夠——應用的啟動時間超出了 &lt;code>healthy_threshold × interval&lt;/code>。兩個解法：加大 &lt;code>healthy_threshold&lt;/code>（拉長開始接流量前的等待），或設一段啟動寬限期（像 ECS 的 &lt;code>startPeriod&lt;/code>）讓健康檢查在應用初始化期間暫停、不把初始化中的失敗計入。啟動期該跟穩定運行期用不同的容忍窗口，這對應 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">模組四 startup 探針&lt;/a> 的語意：啟動中是一種獨立的健康狀態，不該用穩定期的標準去判。&lt;/p>
&lt;h2 id="健康檢查決定後端清單但清單會滯後">健康檢查決定後端清單，但清單會滯後&lt;/h2>
&lt;p>在編排平台上，健康檢查跟服務發現是綁定的：Kubernetes 把 endpoint 跟 readiness 探針綁定，一個 pod 的 readiness 通過，它的 IP 才被加進 Endpoints、才進入 LB 的後端清單。readiness 的定義因此直接決定了流量會不會進來——這是「健康檢查決定後端清單」最直接的機制。&lt;/p>
&lt;p>LB 拿到的清單會滯後於真實健康狀態，中間有一段傳播延遲。readiness 從通過轉為失敗，要先傳到 endpoint controller、再傳到每個節點的轉發層（kube-proxy 的 iptables/IPVS 或 envoy），這段期間客戶端仍可能打到已經 not-ready 的 pod。這也是為什麼摘除一個實例時，要在真正關閉前留一段等待（preStop 加 5 到 15 秒），讓摘除狀態傳播到所有轉發層再收束——這段的完整處理在 &lt;a href="https://tarrragon.github.io/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">模組四 graceful shutdown&lt;/a> 的退場順序。用 DNS 做服務發現時滯後更明顯：DNS A record 不帶健康狀態，回的 IP 可能對應一個已經 not-ready 但還沒被移除的後端，加上 DNS TTL 與客戶端快取，健康狀態的更新要更久才傳到。這裡有個特別危險的邊界——TTL 常見預設 30 秒，但某些 JVM 客戶端的 DNS 快取預設是永不過期，一旦快取到一個壞掉的 IP 就再也不重查，滯後從「幾十秒」變成「直到重啟」。&lt;/p></description><content:encoded><![CDATA[<p>健康檢查路由決定負載平衡把流量送給哪些後端、不送給哪些。設計沿兩個時間窗口展開：一個新後端要多久才開始接流量、一個壞後端要多久才被移出輪替。兩個窗口都可能設錯，且錯的方向相反——太寬鬆，壞掉的後端留在輪替裡繼續讓部分使用者收到錯誤；太嚴格，部署瞬間還在初始化的新後端就被判死，反覆移出移入、使用者看到間歇性失敗。</p>
<p>這一章講的是負載平衡端怎麼消費健康訊號（多久探一次、幾次算數、探到不健康怎麼辦）。服務端怎麼提供健康訊號、端點回什麼才算健康、check 要探到多深，是 <a href="/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">模組四的 health check endpoint 設計</a>——服務怎麼提供跟 LB 怎麼使用是同一個概念的兩側，這裡不重述服務側。</p>
<h2 id="被動探測與主動探測">被動探測與主動探測</h2>
<p>負載平衡判斷後端健康有兩種路徑。主動探測是 LB 定時對後端發一個健康請求（例如每 15 秒 curl 一次 <code>/healthz</code>），依回應判定健康；被動探測是 LB 觀察它轉發的真實流量、不額外發請求——某個後端連續回幾次錯誤或逾時，就把它標成不健康、暫停送流量。</p>
<p>兩者的盲點不同。主動探測的盲點是探測請求跟真實請求走的路徑可能不同：<code>/healthz</code> 通了不代表真實的業務端點通，一個只檢查淺層的探測會漏掉只在真實流量上出現的失敗。被動探測的盲點是它要先犧牲幾個真實請求才發現問題——後端壞掉的當下，是靠幾個真實使用者撞到錯誤，LB 才學到它不健康。成熟的做法常常兩者都用：主動探測抓「後端整個沒回應」，被動探測抓「後端回應了但一直出錯」。雲端 LB（如 ALB）預設走主動探測，開源 nginx 預設走被動探測，主動探測要靠額外機制，這個差別在 <a href="/blog/operations/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置</a> 展開。</p>
<h2 id="閾值定出兩個時間窗口">閾值定出兩個時間窗口</h2>
<p>主動探測的行為由幾個參數的交互決定，它們算術地定出前面說的兩個窗口。以一組具體的 ALB 設定為例：<code>interval = 15</code>（每 15 秒探一次）、<code>healthy_threshold = 2</code>（連續 2 次通過才算健康）、<code>unhealthy_threshold = 3</code>（連續 3 次失敗才算壞）、<code>timeout = 5</code>（單次探測 5 秒沒回算失敗）。</p>
<p>這組參數的意思是：一個新後端要等 <code>2 × 15 = 30</code> 秒（兩次通過）才開始接流量，一個壞掉的後端要 <code>3 × 15 = 45</code> 秒（三次失敗）才被移出。每個參數的鬆緊都是雙向取捨：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>過小的風險</th>
          <th>過大的風險</th>
          <th>起點建議</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>interval</code></td>
          <td>探測本身對後端造成額外負擔</td>
          <td>壞後端被偵測到的延遲增加</td>
          <td>15-30 秒</td>
      </tr>
      <tr>
          <td><code>healthy_threshold</code></td>
          <td>還沒完全就緒就接流量</td>
          <td>部署後等太久才開始分流</td>
          <td>2-3 次</td>
      </tr>
      <tr>
          <td><code>unhealthy_threshold</code></td>
          <td>暫時性波動導致健康的後端被移出</td>
          <td>壞後端繼續收流量太久</td>
          <td>2-3 次</td>
      </tr>
      <tr>
          <td><code>timeout</code></td>
          <td>正常但偏慢的回應被誤判為失敗</td>
          <td>確實掛了卻要等很久才確認</td>
          <td>5 秒</td>
      </tr>
  </tbody>
</table>
<p><code>unhealthy_threshold</code> 設 1 的問題最典型：一次網路抖動、一個偶發的慢回應，就足以把一個健康的後端踢出去，造成不必要的容量波動。要幾次連續失敗才算數，是在「快速摘除壞後端」跟「容忍暫時性抖動」之間取平衡。</p>
<p><code>interval</code> 的取捨在探測頻率與探測負擔之間：探得太密，健康檢查本身變成後端的額外負載（每台都要固定回應探測）；探得太疏，壞後端要更久才被偵測到。<code>timeout</code> 則要對齊後端正常回應的時間分佈：設得比正常的慢回應還短，會把偶爾偏慢但其實健康的後端誤判成失敗、把它踢出去；設得太長，一台確實掛了的後端要拖很久才確認。這兩個參數跟兩個 threshold 相乘，才共同定出前面那兩個窗口的實際長度。</p>
<h2 id="flapping部署瞬間的移出移入">flapping：部署瞬間的移出移入</h2>
<p>健康檢查最常見的失效表現是 flapping——每次部署時，新後端在健康與不健康之間反覆跳動。根因幾乎都是同一個：應用的啟動時間超過了「開始接流量」的窗口。新容器起來、應用還在初始化（載入索引、建連線池），但健康檢查已經開始探測，探到還沒就緒的應用、判不健康、移出；應用初始化完又通過、移入；下一次探測可能又撞上某個還沒好的部分。</p>
<p>判讀方式是觀察後端在健康與不健康之間的轉換次數。每次部署都看到新後端跳動，代表初始等待不夠——應用的啟動時間超出了 <code>healthy_threshold × interval</code>。兩個解法：加大 <code>healthy_threshold</code>（拉長開始接流量前的等待），或設一段啟動寬限期（像 ECS 的 <code>startPeriod</code>）讓健康檢查在應用初始化期間暫停、不把初始化中的失敗計入。啟動期該跟穩定運行期用不同的容忍窗口，這對應 <a href="/blog/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">模組四 startup 探針</a> 的語意：啟動中是一種獨立的健康狀態，不該用穩定期的標準去判。</p>
<h2 id="健康檢查決定後端清單但清單會滯後">健康檢查決定後端清單，但清單會滯後</h2>
<p>在編排平台上，健康檢查跟服務發現是綁定的：Kubernetes 把 endpoint 跟 readiness 探針綁定，一個 pod 的 readiness 通過，它的 IP 才被加進 Endpoints、才進入 LB 的後端清單。readiness 的定義因此直接決定了流量會不會進來——這是「健康檢查決定後端清單」最直接的機制。</p>
<p>LB 拿到的清單會滯後於真實健康狀態，中間有一段傳播延遲。readiness 從通過轉為失敗，要先傳到 endpoint controller、再傳到每個節點的轉發層（kube-proxy 的 iptables/IPVS 或 envoy），這段期間客戶端仍可能打到已經 not-ready 的 pod。這也是為什麼摘除一個實例時，要在真正關閉前留一段等待（preStop 加 5 到 15 秒），讓摘除狀態傳播到所有轉發層再收束——這段的完整處理在 <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">模組四 graceful shutdown</a> 的退場順序。用 DNS 做服務發現時滯後更明顯：DNS A record 不帶健康狀態，回的 IP 可能對應一個已經 not-ready 但還沒被移除的後端，加上 DNS TTL 與客戶端快取，健康狀態的更新要更久才傳到。這裡有個特別危險的邊界——TTL 常見預設 30 秒，但某些 JVM 客戶端的 DNS 快取預設是永不過期，一旦快取到一個壞掉的 IP 就再也不重查，滯後從「幾十秒」變成「直到重啟」。</p>
<h2 id="lb-顯示-healthy-不等於服務可服務">LB 顯示 healthy 不等於服務可服務</h2>
<p>健康檢查通過是一個節點層的訊號，它不保證那個節點能承受當前的流量。最常見的誤判是把「LB 顯示節點 healthy」當成「服務可承受流量」——健康檢查通常只是一個定期的淺探測通過，跟「這個節點扛得住突然湧入的重連潮」是不同層級的問題。</p>
<p>事故裡要把兩層訊號分開看。節點層健康是健康檢查 pass；連線層健康是重連率、長連線錯誤率、tail latency 這些反映「節點實際承受得了流量嗎」的訊號。一個剛通過健康檢查、被 LB 加回輪替的節點，可能立刻被積壓的重連潮壓垮——健康檢查說它好了，連線層訊號說它扛不住。health contract 要盡量反映服務的真實可服務狀態（含依賴連線池、必要 config、關鍵背景任務），而不是停在單一淺探測成功；但即使如此，節點層跟連線層仍是兩個要分開判讀的訊號。這條判讀在切流事故裡尤其關鍵，完整脈絡見 <a href="/blog/backend/05-deployment-platform/load-balancer-contract/" data-link-title="5.3 load balancer 合約" data-link-desc="整理 idle timeout、draining 與 health check">backend 的 load balancer 合約</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>入口層的整體地圖（DNS → 負載平衡 → reverse proxy → 應用的責任鏈、TLS 終結位置、單機與雲端入口的選擇條件）→ <a href="/blog/infra/03-network-foundation/traffic-entry-layer/" data-link-title="流量入口層 — 請求怎麼從使用者到達應用" data-link-desc="自管環境要決定請求從使用者到應用經過哪幾層入口、每層承擔什麼——TLS 終結的位置、動靜分離、單機 reverse proxy 與雲端負載平衡器的選擇">infra：流量入口層</a></li>
<li>服務端怎麼提供健康訊號、端點探多深 → <a href="/blog/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">模組四 Health check endpoint 設計</a></li>
<li>在 nginx 上配被動與主動健康檢查、以及開源版的限制 → <a href="/blog/operations/01-load-balancing/nginx-configuration/" data-link-title="nginx 實務配置" data-link-desc="用 nginx 當反向代理配 upstream、負載分散方法、健康檢查與 timeout 時，特別是要知道開源版的主動健康檢查限制與 proxy header 陷阱時回來讀">nginx 實務配置</a></li>
<li>健康檢查通過的實例，怎麼被演算法選中 → <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a></li>
<li>摘除實例時的傳播延遲與退場順序 → <a href="/blog/operations/04-service-health/graceful-shutdown/" data-link-title="Graceful shutdown" data-link-desc="設計服務收到停止信號後的收束流程時，釐清 SIGTERM 到 SIGKILL 的 grace period、退場的固定順序、以及不同 workload 的 drain 窗口要留多長">模組四 Graceful shutdown</a></li>
</ul>
]]></content:encoded></item><item><title>LB 是水平擴展的前提</title><link>https://tarrragon.github.io/blog/operations/01-load-balancing/scaling-prerequisite/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/01-load-balancing/scaling-prerequisite/</guid><description>&lt;p>水平擴展是靠加開實例來分攤流量，但加實例本身不會自動讓服務擴得動，它要兩個前提同時成立：流量分得進新實例、以及任何實例都能接任何請求。負載平衡解第一個——它是那個把流量分到多個實例的元件，沒有它，新開的實例是一台沒有流量進得去的孤島。無狀態解第二個——實例之間不能有「只有我這台知道」的狀態，否則流量分過去也服務不了。這一章講負載平衡怎麼滿足第一個前提、以及第二個前提為什麼把設計推向無狀態，把「怎麼做到無狀態」交棒給水平擴展模組。&lt;/p>
&lt;h2 id="lb-讓新實例能接到流量">LB 讓新實例能接到流量&lt;/h2>
&lt;p>一個新開的實例要能接流量，得先讓負載平衡認得它。流程是實例啟動、註冊進後端群組、健康檢查通過、開始被分流。這條路徑上任何一環沒接好，實例就擴而不用——機器開了、資源在燒，但流量進不去。&lt;/p>
&lt;p>擴容時最常見的訊號是「新實例遲遲不接流量」。根因通常在註冊與健康檢查的時序：實例註冊進服務發現有延遲、健康檢查要連續通過幾次才開始分流、傳播到所有轉發層又要一段時間，這些疊起來就是新實例從啟動到真正分到流量的空窗。這段空窗在 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計&lt;/a> 有完整的傳播延遲鏈；對擴展來說，關鍵是加實例到它真正幫上忙之間有延遲，容量規劃不能假設按下擴容就立刻有容量。&lt;/p>
&lt;h2 id="無狀態讓任何實例能接任何請求">無狀態讓任何實例能接任何請求&lt;/h2>
&lt;p>第二個前提是任何實例都能接任何請求。這在實例本地保存了請求相關狀態時會破功——會話資料存在某台實例的記憶體裡，那個會話的後續請求就只能回到那台，負載平衡沒辦法自由地把它分給別台。要維持這種綁定，就得靠 &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">會話黏著&lt;/a>，而黏著的代價是負載不均、以及那台實例掛掉時會話狀態一起消失。&lt;/p>
&lt;p>把狀態從實例本地移出去，這兩個代價就消失。會話資料放進外部的共享儲存（例如集中的 session store），每個實例都是無狀態的——它不保存任何「只有我這台知道」的狀態，需要時每次從共享儲存讀。這樣負載平衡可以把任何請求分給任何實例，加開的實例立刻是完全對等的容量，掛掉一台也不會帶走誰的會話。無狀態是讓水平擴展真正線性的關鍵：實例對等，加 N 台就多 N 台的容量。&lt;/p>
&lt;p>外置狀態不是沒有代價——它把「本地記憶體讀取」換成「網路讀共享儲存」，多一次網路往返、也讓共享儲存成為新的關鍵依賴。共享儲存怎麼選、會話怎麼處理、哪些狀態能外置哪些不能，是 &lt;a href="https://tarrragon.github.io/blog/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">水平擴展模組&lt;/a> 的主題，這裡只確立「無狀態是水平擴展的前提」這條方向。&lt;/p>
&lt;h2 id="擴縮時網路規則要跟著成員身分走">擴縮時，網路規則要跟著成員身分走&lt;/h2>
&lt;p>實例會隨擴縮不斷增減、換 IP，網路的存取規則不能綁在具體 IP 上，否則每擴一台就要改一次規則。可靠的做法是讓規則跟著成員身分走：後端的防火牆規則寫成「只接受來自負載平衡那一群的流量」，而不是「只接受這幾個 IP」。這樣從 2 台擴到 20 台，成員換了一輪 IP，規則一個字都不用改。這套 group 對 group 的引用是 &lt;a href="https://tarrragon.github.io/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">infra 網路地基&lt;/a> 的設計，對水平擴展的意義是它讓擴縮不牽動網路規則。&lt;/p>
&lt;h2 id="跨可用區分流是容錯的地基">跨可用區分流是容錯的地基&lt;/h2>
&lt;p>負載平衡把流量分到多個實例時，這些實例最好分散在不同的可用區（AZ），這樣單一 AZ 故障時，其他區的實例還能承接。基礎設施層負責把跨 AZ 的網路地圖鋪對稱——每種角色在至少兩個可用區各有落點；負載平衡層負責把入口流量實際分派到跨可用區的後端。infra 的網路地基明確把「入口流量怎麼分到跨可用區的後端」這件事交給負載平衡處理，兩層在這裡接棒。跨 AZ、跨區域的容錯設計本身是高可用的範圍，會在 &lt;a href="https://tarrragon.github.io/blog/operations/06-high-availability/" data-link-title="模組六：高可用與災難復原" data-link-desc="一個節點掛了服務怎麼不中斷 — 冗餘設計、failover 機制、disaster recovery 策略">模組六&lt;/a> 展開，這裡的重點是負載平衡的分流能力是那層容錯的地基。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>怎麼做到無狀態、會話怎麼處理、共享儲存怎麼選 → &lt;a href="https://tarrragon.github.io/blog/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">模組二 水平擴展&lt;/a>&lt;/li>
&lt;li>黏著與外置狀態的取捨、演算法怎麼選 → &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法&lt;/a>&lt;/li>
&lt;li>新實例從啟動到接流量的傳播延遲 → &lt;a href="https://tarrragon.github.io/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計&lt;/a>&lt;/li>
&lt;li>跨可用區與跨區域的容錯設計 → &lt;a href="https://tarrragon.github.io/blog/operations/06-high-availability/" data-link-title="模組六：高可用與災難復原" data-link-desc="一個節點掛了服務怎麼不中斷 — 冗餘設計、failover 機制、disaster recovery 策略">模組六 高可用&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>水平擴展是靠加開實例來分攤流量，但加實例本身不會自動讓服務擴得動，它要兩個前提同時成立：流量分得進新實例、以及任何實例都能接任何請求。負載平衡解第一個——它是那個把流量分到多個實例的元件，沒有它，新開的實例是一台沒有流量進得去的孤島。無狀態解第二個——實例之間不能有「只有我這台知道」的狀態，否則流量分過去也服務不了。這一章講負載平衡怎麼滿足第一個前提、以及第二個前提為什麼把設計推向無狀態，把「怎麼做到無狀態」交棒給水平擴展模組。</p>
<h2 id="lb-讓新實例能接到流量">LB 讓新實例能接到流量</h2>
<p>一個新開的實例要能接流量，得先讓負載平衡認得它。流程是實例啟動、註冊進後端群組、健康檢查通過、開始被分流。這條路徑上任何一環沒接好，實例就擴而不用——機器開了、資源在燒，但流量進不去。</p>
<p>擴容時最常見的訊號是「新實例遲遲不接流量」。根因通常在註冊與健康檢查的時序：實例註冊進服務發現有延遲、健康檢查要連續通過幾次才開始分流、傳播到所有轉發層又要一段時間，這些疊起來就是新實例從啟動到真正分到流量的空窗。這段空窗在 <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a> 有完整的傳播延遲鏈；對擴展來說，關鍵是加實例到它真正幫上忙之間有延遲，容量規劃不能假設按下擴容就立刻有容量。</p>
<h2 id="無狀態讓任何實例能接任何請求">無狀態讓任何實例能接任何請求</h2>
<p>第二個前提是任何實例都能接任何請求。這在實例本地保存了請求相關狀態時會破功——會話資料存在某台實例的記憶體裡，那個會話的後續請求就只能回到那台，負載平衡沒辦法自由地把它分給別台。要維持這種綁定，就得靠 <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">會話黏著</a>，而黏著的代價是負載不均、以及那台實例掛掉時會話狀態一起消失。</p>
<p>把狀態從實例本地移出去，這兩個代價就消失。會話資料放進外部的共享儲存（例如集中的 session store），每個實例都是無狀態的——它不保存任何「只有我這台知道」的狀態，需要時每次從共享儲存讀。這樣負載平衡可以把任何請求分給任何實例，加開的實例立刻是完全對等的容量，掛掉一台也不會帶走誰的會話。無狀態是讓水平擴展真正線性的關鍵：實例對等，加 N 台就多 N 台的容量。</p>
<p>外置狀態不是沒有代價——它把「本地記憶體讀取」換成「網路讀共享儲存」，多一次網路往返、也讓共享儲存成為新的關鍵依賴。共享儲存怎麼選、會話怎麼處理、哪些狀態能外置哪些不能，是 <a href="/blog/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">水平擴展模組</a> 的主題，這裡只確立「無狀態是水平擴展的前提」這條方向。</p>
<h2 id="擴縮時網路規則要跟著成員身分走">擴縮時，網路規則要跟著成員身分走</h2>
<p>實例會隨擴縮不斷增減、換 IP，網路的存取規則不能綁在具體 IP 上，否則每擴一台就要改一次規則。可靠的做法是讓規則跟著成員身分走：後端的防火牆規則寫成「只接受來自負載平衡那一群的流量」，而不是「只接受這幾個 IP」。這樣從 2 台擴到 20 台，成員換了一輪 IP，規則一個字都不用改。這套 group 對 group 的引用是 <a href="/blog/infra/03-network-foundation/" data-link-title="模組三：網路地基 — VPC 與分層" data-link-desc="VPC、public / private subnet 切分、route table、NAT、security group 設計">infra 網路地基</a> 的設計，對水平擴展的意義是它讓擴縮不牽動網路規則。</p>
<h2 id="跨可用區分流是容錯的地基">跨可用區分流是容錯的地基</h2>
<p>負載平衡把流量分到多個實例時，這些實例最好分散在不同的可用區（AZ），這樣單一 AZ 故障時，其他區的實例還能承接。基礎設施層負責把跨 AZ 的網路地圖鋪對稱——每種角色在至少兩個可用區各有落點；負載平衡層負責把入口流量實際分派到跨可用區的後端。infra 的網路地基明確把「入口流量怎麼分到跨可用區的後端」這件事交給負載平衡處理，兩層在這裡接棒。跨 AZ、跨區域的容錯設計本身是高可用的範圍，會在 <a href="/blog/operations/06-high-availability/" data-link-title="模組六：高可用與災難復原" data-link-desc="一個節點掛了服務怎麼不中斷 — 冗餘設計、failover 機制、disaster recovery 策略">模組六</a> 展開，這裡的重點是負載平衡的分流能力是那層容錯的地基。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>怎麼做到無狀態、會話怎麼處理、共享儲存怎麼選 → <a href="/blog/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">模組二 水平擴展</a></li>
<li>黏著與外置狀態的取捨、演算法怎麼選 → <a href="/blog/operations/01-load-balancing/load-balancing-algorithms/" data-link-title="負載分散演算法" data-link-desc="在 round-robin、least-connections、IP hash、consistent hash 之間選負載分散演算法時，用請求成本是否均勻、實例是否同質、需不需要親和性當判準">負載分散演算法</a></li>
<li>新實例從啟動到接流量的傳播延遲 → <a href="/blog/operations/01-load-balancing/health-check-routing/" data-link-title="健康檢查路由設計" data-link-desc="設計負載平衡的健康檢查時，釐清被動與主動探測的差別、interval 與 threshold 怎麼定出兩個時間窗口、以及為什麼 LB 顯示 healthy 不等於服務可服務">健康檢查路由設計</a></li>
<li>跨可用區與跨區域的容錯設計 → <a href="/blog/operations/06-high-availability/" data-link-title="模組六：高可用與災難復原" data-link-desc="一個節點掛了服務怎麼不中斷 — 冗餘設計、failover 機制、disaster recovery 策略">模組六 高可用</a></li>
</ul>
]]></content:encoded></item></channel></rss>