<?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/backend/09-performance-capacity/</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>Tue, 12 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/backend/09-performance-capacity/index.xml" rel="self" type="application/rss+xml"/><item><title>9.1 壓測理論與系統行為</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>壓測理論的角色是讓「加機器能不能解決」這個問題從直覺變成可推導。沒有理論基礎時、容量決策容易陷入「跑壓測 → 看數字 → 加機器」的盲試循環；有理論之後、可以從「現在的延遲 / 吞吐 / 並發量」反推「瓶頸在哪個資源、加什麼有效」。&lt;/p>
&lt;p>本章是 9.2-9.12 的共同基礎。後續章節的 workload modeling、saturation discovery、capacity planning、SLO 都會回引本章的數學工具。讀者可以把這章當作「容量規劃的最小詞彙表」、其他章節是這些詞彙的應用情境。&lt;/p>
&lt;p>本章不深入推導公式、聚焦在 &lt;em>工程意義&lt;/em>。讀完之後讀者能回答：為什麼系統在 80% utilization 就該擴、為什麼加機器會邊際效益遞減、為什麼 sub-ms 延遲需求會反推架構選擇。&lt;/p>
&lt;h2 id="littles-law穩態系統的最小數學工具">Little&amp;rsquo;s Law：穩態系統的最小數學工具&lt;/h2>
&lt;p>Little&amp;rsquo;s Law 用一條等式 L = λW 把三個變數綁在一起：L 是系統內平均並發數、λ 是請求到達率、W 是請求平均逗留時間。這個關係在 &lt;em>穩態&lt;/em>（流量已穩定、不在 warmup 階段）必然成立、不需要假設特定分布或服務模式。&lt;/p>
&lt;p>工程上最有價值的用法是「反推」。給定預期 RPS λ = 1000 跟 SLO latency 上限 W = 200ms、能算出系統最大穩態並發 L = 1000 × 0.2 = 200。這個 200 直接對應「connection pool size」「thread pool size」「async worker count」這類容量參數 — 訂得比 200 小、系統撐不住預期流量；訂得比 200 大太多、資源浪費。&lt;/p>
&lt;p>反向也成立。當 connection pool 卡死在某個 size L、latency budget W 已訂、能算出可支撐的 RPS。這個算法在 capacity planning 階段比 ramp-up 壓測更快、可以先用 Little&amp;rsquo;s Law 篩掉明顯撐不住的配置、再用壓測驗證剩下的候選。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms&lt;/a> 把 W 訂在 sub-millisecond、所有架構選擇都從這個 W 反推；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi ML p99 &amp;lt; 10ms&lt;/a> 從 W 反推 feature lookup 必須 cache hit 路徑、不能回到持久 store。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/little-law/" data-link-title="Little&amp;#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&amp;rsquo;s Law 卡片&lt;/a>。&lt;/p>
&lt;h2 id="queueing-theory為什麼-80-利用率就是-knee">Queueing Theory：為什麼 80% 利用率就是 knee&lt;/h2>
&lt;p>排隊論（M/M/c 模型）解釋了一個常見直覺：「系統在 50% utilization 看似還很閒、80% 就該擴、90% 已經太晚」。這個直覺不是經驗法則、是 &lt;em>數學必然&lt;/em>。&lt;/p>
&lt;p>M/M/c 系統的平均 queue length 跟 utilization 之間是非線性關係。當 utilization 從 50% 漲到 70%、queue length 約增加 2-3 倍；從 70% 漲到 90%、queue length 增加 10 倍以上。latency 跟 queue length 成正比（Little&amp;rsquo;s Law 又出現）、所以 latency 也呈現同樣的指數成長。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>壓測理論的角色是讓「加機器能不能解決」這個問題從直覺變成可推導。沒有理論基礎時、容量決策容易陷入「跑壓測 → 看數字 → 加機器」的盲試循環；有理論之後、可以從「現在的延遲 / 吞吐 / 並發量」反推「瓶頸在哪個資源、加什麼有效」。</p>
<p>本章是 9.2-9.12 的共同基礎。後續章節的 workload modeling、saturation discovery、capacity planning、SLO 都會回引本章的數學工具。讀者可以把這章當作「容量規劃的最小詞彙表」、其他章節是這些詞彙的應用情境。</p>
<p>本章不深入推導公式、聚焦在 <em>工程意義</em>。讀完之後讀者能回答：為什麼系統在 80% utilization 就該擴、為什麼加機器會邊際效益遞減、為什麼 sub-ms 延遲需求會反推架構選擇。</p>
<h2 id="littles-law穩態系統的最小數學工具">Little&rsquo;s Law：穩態系統的最小數學工具</h2>
<p>Little&rsquo;s Law 用一條等式 L = λW 把三個變數綁在一起：L 是系統內平均並發數、λ 是請求到達率、W 是請求平均逗留時間。這個關係在 <em>穩態</em>（流量已穩定、不在 warmup 階段）必然成立、不需要假設特定分布或服務模式。</p>
<p>工程上最有價值的用法是「反推」。給定預期 RPS λ = 1000 跟 SLO latency 上限 W = 200ms、能算出系統最大穩態並發 L = 1000 × 0.2 = 200。這個 200 直接對應「connection pool size」「thread pool size」「async worker count」這類容量參數 — 訂得比 200 小、系統撐不住預期流量；訂得比 200 大太多、資源浪費。</p>
<p>反向也成立。當 connection pool 卡死在某個 size L、latency budget W 已訂、能算出可支撐的 RPS。這個算法在 capacity planning 階段比 ramp-up 壓測更快、可以先用 Little&rsquo;s Law 篩掉明顯撐不住的配置、再用壓測驗證剩下的候選。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> 把 W 訂在 sub-millisecond、所有架構選擇都從這個 W 反推；<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi ML p99 &lt; 10ms</a> 從 W 反推 feature lookup 必須 cache hit 路徑、不能回到持久 store。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/little-law/" data-link-title="Little&#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&rsquo;s Law 卡片</a>。</p>
<h2 id="queueing-theory為什麼-80-利用率就是-knee">Queueing Theory：為什麼 80% 利用率就是 knee</h2>
<p>排隊論（M/M/c 模型）解釋了一個常見直覺：「系統在 50% utilization 看似還很閒、80% 就該擴、90% 已經太晚」。這個直覺不是經驗法則、是 <em>數學必然</em>。</p>
<p>M/M/c 系統的平均 queue length 跟 utilization 之間是非線性關係。當 utilization 從 50% 漲到 70%、queue length 約增加 2-3 倍；從 70% 漲到 90%、queue length 增加 10 倍以上。latency 跟 queue length 成正比（Little&rsquo;s Law 又出現）、所以 latency 也呈現同樣的指數成長。</p>
<p>工程意義：健康系統運轉在 50-70% utilization、超過 80% 就接近 knee、超過 90% 進入不可預測區。「為什麼明明還沒滿就 saturate」的答案就在這條曲線。autoscaler 的 target metric 通常訂在 60-70%、是 queueing theory 推導出的安全邊界、不是工程師憑感覺。</p>
<p>多 server 模型（M/M/c）比單 server（M/M/1）有顯著容量優勢：c 個 server 的有效容量遠超 1 個 server 容量 × c。這也解釋了為什麼水平擴容（多開幾個 instance）通常比垂直擴容（單機加 CPU）划算 — 不只是規模、是 queue 行為的本質差異。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 25ms p95</a> 把 p95 維持在 25ms 同時撐 54K TPS、靠的是 <em>永遠不讓系統進入 knee</em>、AI 預測讓擴容窗口縮短到 reaction time 內。</p>
<h2 id="universal-scalability-law擴容會邊際失效">Universal Scalability Law：擴容會邊際失效</h2>
<p>USL（Neil Gunther 提出）的公式 throughput(N) = N / (1 + α(N-1) + βN(N-1)) 解釋了「為什麼加機器到某個點之後 throughput 反而下降」。兩個常數 α 跟 β 描述系統的擴展限制：</p>
<ul>
<li>α 是必須序列化的部分（Amdahl&rsquo;s Law 的對應）。distributed lock、coordinator、單一 leader DB 都是 α 來源。α 越大、線性擴容越早 plateau。</li>
<li>β 是節點間互相通訊的成本（crosstalk）。cache invalidation broadcast、consensus <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>、cross-region replication 都是 β。β 比 α 更危險、會讓 throughput 在 N 大到某點後 <em>反向下降</em>。</li>
</ul>
<p>工程上 α 比較好處理 — 把序列化部分拆細、用 partition 切分、用 sharded coordinator。β 比較難 — 通訊本質就需要協調、降低 β 通常要重新設計分散式協議（例如 Spanner 用 TrueTime 把跨節點交易的協調成本降低）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner 線性擴展到 10 億 req/sec</a> — TrueTime API 讓跨地區交易的 β 降到可接受、達成傳統 OLTP 做不到的線性；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase RAFT consensus</a> — RAFT 的 quorum 通訊讓 β 不可降、所以 <em>選擇不橫向擴</em>、改用 z1d + Cluster Placement Group 榨單機。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">USL 卡片</a>。</p>
<h2 id="saturation-curvelinear--knee--cliff">Saturation Curve：linear → knee → cliff</h2>
<p>實際系統的 latency vs throughput 曲線分三段。第一段是 linear region — utilization 低、latency 平穩、加流量幾乎不影響 latency。第二段是 knee — utilization 接近 80%、latency 開始指數成長、再加流量會明顯變慢。第三段是 cliff — 系統進入不穩定區、latency 不可預測、可能 timeout、可能 cascade failure。</p>
<p>容量規劃的關鍵概念是 <em>knee point = 設計容量上限</em>。健康系統運轉在 knee 以下 50-70%、留出 headroom 應付 burst 跟 forecast 誤差。沒有量過 knee 的系統等於「不知道距離崩潰多遠」 — 平日看起來穩、實際隨時可能因為一個小 spike 進入 cliff。</p>
<p>不同 system 的 knee 位置差異很大。stateless service 通常 knee 在 80% CPU；DB 因為 lock contention、knee 可能在 60% utilization；broker / queue 因為 disk I/O bottleneck、knee 可能在 50%。容量規劃時不能一概而論、必須個別量測。</p>
<p>每次重大改動後必須 re-test knee。新增功能、改 ORM、升級 library、調 GC tuning、改 cache 策略 — 任何一個都可能讓 knee 往不好的方向移。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft DynamoDB IOPS 20 → 135K</a> — partition 設計均勻時 saturation point 可以推到極遠（6750x 擴展）；<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9000 萬 RPS</a> — 線性擴展靠 partition key 均勻、不靠 vendor 神話。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point 卡片</a>。</p>
<h2 id="反推從業務-kpi-到系統參數">反推：從業務 KPI 到系統參數</h2>
<p>理論工具的真正價值在「反推」 — 不是先設計系統再量測 saturate 多少、是 <em>先訂業務目標再反推系統參數</em>。這層思維把容量規劃從 reactive（撐到撐不住才擴）變成 proactive（按業務需求預先配置）。</p>
<p>反推流程通常從 latency budget 開始（詳見 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>）：</p>
<ol>
<li>從 user-perceived end-to-end latency（例如 p99 500ms）開始</li>
<li>拆到每個 stage（網路、CDN、application、cache、DB、第三方）的 latency 配額</li>
<li>配額決定每個 stage 的設計選擇 — DB 配 50ms → 不能跨 region、application 配 100ms → 不能多層 microservice hop</li>
<li>配額 + 預期 RPS → Little&rsquo;s Law 算每個 stage 的並發</li>
<li>並發 → 每個 stage 的容量需求 → 實例數 / connection pool size / cache size</li>
</ol>
<p>反推失敗的常見徵兆：算出來的某個 stage 容量超過 vendor 提供的上限（例如「需要 50 萬 DynamoDB RCU」可能超過單一 table partition 上限）、或某個 stage latency 配額過短（例如 cross-AZ 網路至少 1-2ms、配 0.5ms 不可能達成）。這時要回頭調整 SLO 或重新設計架構。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget 卡片</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>sub-ms latency 反推所有架構選擇</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>TrueTime 降低 β 達成線性擴展</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>ML p99 &lt; 10ms 的 stage latency 配額</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>線性擴展靠 partition 均勻、不靠魔法</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>下游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a>（把模型量化成 production traffic）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（實測 knee point）</li>
<li>跨章節：<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a>（latency budget 拆解）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/little-law/" data-link-title="Little&#39;s Law" data-link-desc="說明系統內並發數、到達率與逗留時間三者的數學關係">Little&rsquo;s Law</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
</ul>
]]></content:encoded></item><item><title>9.2 Workload Modeling</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Workload modeling 的角色是讓壓測結果有意義。如果壓測模型跟 production traffic shape 不一致、壓測通過不代表 production 撐得住。這一層的工作不是「製造大量請求」、而是「製造跟 production 一樣形狀的請求」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&amp;#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論&lt;/a> 的關係：9.1 提供推導工具、9.2 把工具的輸入（流量參數）量化。沒有 workload model、Little&amp;rsquo;s Law 的 λ 跟 W 都是猜。&lt;/p>
&lt;p>本章的核心問題：production traffic 不是「N RPS」這麼簡單。它有時間分布、地理分布、操作分布、cohort 分布、burst pattern。每個維度都會影響系統行為。一個只測「總 RPS」的壓測通過了、production 還是可能因為某個 cohort 集中或某個 burst pattern 出事。&lt;/p>
&lt;h2 id="traffic-shape-的五個維度">Traffic shape 的五個維度&lt;/h2>
&lt;p>Production traffic shape 至少要量五個維度才算 model 完整。&lt;/p>
&lt;p>&lt;strong>平均吞吐 vs 峰值&lt;/strong>：peak/avg ratio 是工程意義最大的單一指標。1.5x 的 peak/avg 代表流量相對平緩、容量規劃可以接近 average peak；3-5x 的 peak/avg 代表 bursty 流量、必須按 peak 規劃、平日大幅 over-provision。對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday 24h 1.67 億 / 峰值 3500 RPS&lt;/a> 峰均比約 1.81x 屬於相對溫和；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">Tixcraft 5 分鐘賣完&lt;/a> 是另一極端。&lt;/p>
&lt;p>&lt;strong>時間分布&lt;/strong>：日內（早晚通勤）、週內（週末活躍）、月內（月初發薪）、季內（節慶）、年內（活動）。不同尺度的週期都要記錄、用於 forecast 跟 pre-scaling 決策。&lt;/p>
&lt;p>&lt;strong>用戶分布&lt;/strong>：geographic（哪個 region 多）、device（mobile vs desktop）、tier（free / paid / VIP）。同樣 RPS、不同分布可能造成完全不同系統行為 — VIP 用戶可能跑更複雜 query、mobile 用戶可能更多 retry、跨 region 用戶可能更多 cross-zone latency。&lt;/p>
&lt;p>&lt;strong>操作分布&lt;/strong>：read vs write 比、不同 endpoint 的 mix。一個系統 90% read 跟 50% read 的容量設計完全不同 — read-heavy 可以 cache、write-heavy 必須關注 storage IOPS。&lt;/p>
&lt;p>&lt;strong>Cohort 與 burst pattern&lt;/strong>：同一秒的請求不一定均勻 — bursty arrival 比 Poisson arrival 對系統更殘酷。突發 burst 來源：promo 推播、KOL 推廣、新片發布、新聞事件。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &amp;#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 賽事高潮 burst&lt;/a> — 賽事「進球瞬間」 burst 比平均流量高 10-50 倍；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&amp;#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&amp;#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布&lt;/a> — 同片瞬間集中、cohort 高度集中。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Workload modeling 的角色是讓壓測結果有意義。如果壓測模型跟 production traffic shape 不一致、壓測通過不代表 production 撐得住。這一層的工作不是「製造大量請求」、而是「製造跟 production 一樣形狀的請求」。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> 的關係：9.1 提供推導工具、9.2 把工具的輸入（流量參數）量化。沒有 workload model、Little&rsquo;s Law 的 λ 跟 W 都是猜。</p>
<p>本章的核心問題：production traffic 不是「N RPS」這麼簡單。它有時間分布、地理分布、操作分布、cohort 分布、burst pattern。每個維度都會影響系統行為。一個只測「總 RPS」的壓測通過了、production 還是可能因為某個 cohort 集中或某個 burst pattern 出事。</p>
<h2 id="traffic-shape-的五個維度">Traffic shape 的五個維度</h2>
<p>Production traffic shape 至少要量五個維度才算 model 完整。</p>
<p><strong>平均吞吐 vs 峰值</strong>：peak/avg ratio 是工程意義最大的單一指標。1.5x 的 peak/avg 代表流量相對平緩、容量規劃可以接近 average peak；3-5x 的 peak/avg 代表 bursty 流量、必須按 peak 規劃、平日大幅 over-provision。對應案例：<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday 24h 1.67 億 / 峰值 3500 RPS</a> 峰均比約 1.81x 屬於相對溫和；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 5 分鐘賣完</a> 是另一極端。</p>
<p><strong>時間分布</strong>：日內（早晚通勤）、週內（週末活躍）、月內（月初發薪）、季內（節慶）、年內（活動）。不同尺度的週期都要記錄、用於 forecast 跟 pre-scaling 決策。</p>
<p><strong>用戶分布</strong>：geographic（哪個 region 多）、device（mobile vs desktop）、tier（free / paid / VIP）。同樣 RPS、不同分布可能造成完全不同系統行為 — VIP 用戶可能跑更複雜 query、mobile 用戶可能更多 retry、跨 region 用戶可能更多 cross-zone latency。</p>
<p><strong>操作分布</strong>：read vs write 比、不同 endpoint 的 mix。一個系統 90% read 跟 50% read 的容量設計完全不同 — read-heavy 可以 cache、write-heavy 必須關注 storage IOPS。</p>
<p><strong>Cohort 與 burst pattern</strong>：同一秒的請求不一定均勻 — bursty arrival 比 Poisson arrival 對系統更殘酷。突發 burst 來源：promo 推播、KOL 推廣、新片發布、新聞事件。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 賽事高潮 burst</a> — 賽事「進球瞬間」 burst 比平均流量高 10-50 倍；<a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布</a> — 同片瞬間集中、cohort 高度集中。</p>
<h2 id="從-production-log-抽-workload-model">從 production log 抽 workload model</h2>
<p>實務上 workload model 不能憑空寫、要從 production data 抽。流程通常分四步：</p>
<p><strong>第一步：data 蒐集</strong>。從 access log、APM trace、metric 系統取得 production traffic 樣本。要 sampling（不是全量）、避免影響 production；要包含 <em>至少一個完整 weekly cycle</em>（含週末、含峰谷）；要按 endpoint / per-tenant 分組。</p>
<p><strong>第二步：分組統計</strong>。對每組（per endpoint、per tier、per region）計算 percentile（p50 / p95 / p99）、arrival pattern（Poisson、bursty、scheduled）、payload size 分布。輸出是「workload profile」 — 比單一數字更接近 reality。</p>
<p><strong>第三步：序列重播</strong>。複製一段 production traffic 的時間序列、保留 inter-arrival timing（不只是 RPS 平均、是 <em>每秒幾個</em>）。這層讓 burst 在壓測重現、不只是「平均壓力均勻分布」。</p>
<p><strong>第四步：脫敏處理</strong>。PII（user_id、phone、address）必須匿名化或替換 — 否則壓測環境變成 PII 洩漏點。常見做法：hash + salt + 確保結果 cardinality 跟 production 一致。</p>
<p>production log 通常缺寫入 payload（log 只記 metadata、不記 request body）、要從 application metric 或 schema sample 補。schema sample 用「distinct value 抽樣」、不是「random」 — 確保壓測涵蓋常見 value pattern。</p>
<h2 id="synthetic-load-vs-production-replay">Synthetic load vs production replay</h2>
<p>兩種主要壓測方式各有取捨。</p>
<p><strong>Synthetic load</strong>：手寫腳本、明確控制每個請求的 shape。優點是好複現、可以針對特定情境設計（例如「測登入失敗 retry」）；缺點是容易脫離 production reality、寫腳本的人會無意識套用自己的偏見。</p>
<p><strong>Production traffic replay</strong>：用 GoReplay、Istio mirror、AWS VPC Traffic Mirroring 等工具把 production traffic 複製到測試環境。優點是 <em>最貼近真實</em>、自動帶上 burst 跟 cohort；缺點是消耗 production 下游資源（要算進容量規劃）、PII / 合規處理複雜、replay 環境的下游 mock 不容易做。</p>
<p><strong>混合模式</strong>：常態壓測用 synthetic（cheap、可控）、release candidate 驗證用 production replay（真實）、debug 特定 incident 用 <em>特定時段</em> 的 replay。三種工具在不同階段用、不是二選一。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel 雙峰需要兩個 workload model 並行</a> — 直播 model（CDN heavy、長 session）跟投注 model（低延遲、burst at goal）必須分開壓測、不能合成一個。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model 卡片</a> 跟 <a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic 卡片</a>。</p>
<h2 id="模型驗證怎麼知道模型像-production">模型驗證：怎麼知道模型像 production</h2>
<p>寫了 workload model 之後、怎麼驗證它真的「像 production」？方法是 <em>跑壓測 同時 對比 production metrics</em>。</p>
<p>驗證指標包含：throughput pattern（總 RPS、各 endpoint mix）、latency 分布（p50 / p95 / p99 對比）、resource utilization（CPU / memory / network 行為）、error rate 與 retry pattern。</p>
<p>兩個可能的偏差結果：</p>
<ul>
<li><strong>模型撐不住但 production 撐得住</strong> → 模型太苛刻、可能高估了流量或操作複雜度。usually fine、調整模型參數即可。</li>
<li><strong>模型撐得住但 production 撐不住</strong> → 模型不足、漏了某個維度。dangerous、需要回到 data 蒐集階段找漏掉的 pattern。</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID surge</a> — 之前的 workload model 完全不能用、必須 reset baseline 重新從 post-COVID 流量抽 model；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — 用實際售票場景重播驗證、不是 synthetic 數字。</p>
<h2 id="模型維護定期-review">模型維護：定期 review</h2>
<p>Workload model 不是一次抽完就永久有效。業務變化會讓模型過時、過時的模型導出的容量規劃會失準。</p>
<p>需要 re-抽 model 的訊號：</p>
<ul>
<li>新功能上線改變 user journey（例如新增 video upload、user 行為變寫多）</li>
<li>新市場進入改變 cohort 分布（例如進入印度市場、mobile share 大幅增加）</li>
<li>行銷活動改變 burst pattern（例如新增 push notification、burst 集中度上升）</li>
<li>用戶習慣轉變（例如 work-from-home 讓週末跟平日流量比變化）</li>
</ul>
<p>維護節奏建議每季 review 一次、重大產品改動立即 re-抽。每次 re-抽要 <em>跟前一版對比</em>、量化變化幅度、決定哪些容量計畫要重新評估。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS Black Friday</a></td>
          <td>持續高峰型 workload（峰均比 1.81x）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>flash-sale 形狀（5 分鐘賣完）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">9.C7 Lyft</a></td>
          <td>100+ 微服務各自 workload model（不能用單一）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>3 億 / 天的峰均比預估</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>雙峰必須兩個 model 並行</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a>（用什麼工具實作 model）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（用 model 跑 ramp-up）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（production log 來源）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve</a></li>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
</ul>
]]></content:encoded></item><item><title>9.3 壓測工具選型</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>壓測工具選型的核心不是「哪個工具最強」、是「哪個工具最貼合本團隊的 workload model 表達能力跟 CI 整合需求」。沒有絕對最好的工具、只有最匹配當前場景的工具。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling&lt;/a> 的關係：9.2 定義 workload 長什麼樣、9.3 找能複製這個樣子的工具。工具選對、壓測結果可信；工具選錯、壓測結果誤導。&lt;/p>
&lt;p>本章不是工具教學、是 &lt;em>選型維度&lt;/em> + 主流工具的 &lt;em>適用情境&lt;/em>。讀者讀完後能回答「我現在這個 workload 該用哪個工具」、而不是「哪個工具最快」。&lt;/p>
&lt;h2 id="六個選型維度">六個選型維度&lt;/h2>
&lt;p>選工具時要按六個維度評估、不能只看「能不能跑 HTTP GET」。&lt;/p>
&lt;p>&lt;strong>腳本表達能力&lt;/strong>：能不能寫複雜 user journey（登入 → 瀏覽 → 加購物車 → 結帳）、不只是單一 HTTP request。複雜系統的壓測通常是 user journey 級別、單一 endpoint 壓測只能找絕對極限、找不到 cross-endpoint contention。&lt;/p>
&lt;p>&lt;strong>協議支援&lt;/strong>：HTTP / WebSocket / gRPC / TCP / 自家二進位協議。WebSocket 跟 gRPC 是現代後端常見、傳統工具（JMeter、wrk）可能要 plugin 補。&lt;/p>
&lt;p>&lt;strong>規模能力&lt;/strong>：單機可以發多少 RPS、能不能分散式擴容。本機 wrk 可發 10K-50K RPS；分散式 Locust 可發 1M+ RPS。決定因素：CPU 效率、async I/O 模型、是否單機 bound。&lt;/p>
&lt;p>&lt;strong>CI 整合&lt;/strong>：能不能在 PR 上跑 lightweight perf check、結果能不能機器可讀（JSON / Prometheus exposition）、能不能跟 baseline diff。沒有 CI 整合的工具只能做「事件型壓測」、無法做 continuous perf governance。&lt;/p>
&lt;p>&lt;strong>結果分析&lt;/strong>：原生 dashboard（k6 Cloud、Gatling Enterprise）/ Prometheus + Grafana 整合 / 純文字輸出。要看結果分發、團隊成員能不能輕鬆查詢歷史。&lt;/p>
&lt;p>&lt;strong>學習曲線&lt;/strong>：腳本語言（JavaScript / Scala / Python / Go）、團隊熟悉度。工具好但團隊不會用、會變成 1-2 個工程師的孤島技能、流失時整套廢掉。&lt;/p>
&lt;h2 id="主流開源工具對照">主流開源工具對照&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &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>k6&lt;/td>
 &lt;td>JS&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低-中&lt;/td>
 &lt;td>複雜 user journey + CI 整合、現代工具首選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>JMeter&lt;/td>
 &lt;td>XML/GUI&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>中-高&lt;/td>
 &lt;td>企業已有流程、protocol 廣、reluctant 改&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gatling&lt;/td>
 &lt;td>Scala&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>報表精美、Scala 學習門檻&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Locust&lt;/td>
 &lt;td>Python&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>複雜邏輯、Python 生態、單機 throughput 受限&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Vegeta&lt;/td>
 &lt;td>CLI&lt;/td>
 &lt;td>中&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>CLI driven、quick HTTP 壓測&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>wrk/wrk2&lt;/td>
 &lt;td>C&lt;/td>
 &lt;td>高&lt;/td>
 &lt;td>低&lt;/td>
 &lt;td>單機極限 RPS、saturation discovery 用&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>k6&lt;/strong> 是過去 5 年崛起的綜合首選。JavaScript 腳本（前端工程師也能寫）、原生 dashboard、Prometheus exposition、CI 友善。Grafana 收購後生態加速。缺點：複雜 stateful 場景（DB connection pool 共享）需要繞 workaround。&lt;/p>
&lt;p>&lt;strong>JMeter&lt;/strong> 是企業常見的 incumbent。協議支援廣（含 LDAP、JDBC、JMS）、有 GUI 編輯器。缺點：腳本是 XML、版本控制困難；GUI 主要用來生成腳本、實際跑壓測還是要 headless。已經在用的團隊建議繼續、新團隊不必特意選它。&lt;/p>
&lt;p>&lt;strong>Gatling&lt;/strong> 高 throughput 純 async、性能優秀、報表精美。缺點：Scala / Kotlin DSL 學習曲線陡、新版本（11+）改了 DSL 不向後相容。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>壓測工具選型的核心不是「哪個工具最強」、是「哪個工具最貼合本團隊的 workload model 表達能力跟 CI 整合需求」。沒有絕對最好的工具、只有最匹配當前場景的工具。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> 的關係：9.2 定義 workload 長什麼樣、9.3 找能複製這個樣子的工具。工具選對、壓測結果可信；工具選錯、壓測結果誤導。</p>
<p>本章不是工具教學、是 <em>選型維度</em> + 主流工具的 <em>適用情境</em>。讀者讀完後能回答「我現在這個 workload 該用哪個工具」、而不是「哪個工具最快」。</p>
<h2 id="六個選型維度">六個選型維度</h2>
<p>選工具時要按六個維度評估、不能只看「能不能跑 HTTP GET」。</p>
<p><strong>腳本表達能力</strong>：能不能寫複雜 user journey（登入 → 瀏覽 → 加購物車 → 結帳）、不只是單一 HTTP request。複雜系統的壓測通常是 user journey 級別、單一 endpoint 壓測只能找絕對極限、找不到 cross-endpoint contention。</p>
<p><strong>協議支援</strong>：HTTP / WebSocket / gRPC / TCP / 自家二進位協議。WebSocket 跟 gRPC 是現代後端常見、傳統工具（JMeter、wrk）可能要 plugin 補。</p>
<p><strong>規模能力</strong>：單機可以發多少 RPS、能不能分散式擴容。本機 wrk 可發 10K-50K RPS；分散式 Locust 可發 1M+ RPS。決定因素：CPU 效率、async I/O 模型、是否單機 bound。</p>
<p><strong>CI 整合</strong>：能不能在 PR 上跑 lightweight perf check、結果能不能機器可讀（JSON / Prometheus exposition）、能不能跟 baseline diff。沒有 CI 整合的工具只能做「事件型壓測」、無法做 continuous perf governance。</p>
<p><strong>結果分析</strong>：原生 dashboard（k6 Cloud、Gatling Enterprise）/ Prometheus + Grafana 整合 / 純文字輸出。要看結果分發、團隊成員能不能輕鬆查詢歷史。</p>
<p><strong>學習曲線</strong>：腳本語言（JavaScript / Scala / Python / Go）、團隊熟悉度。工具好但團隊不會用、會變成 1-2 個工程師的孤島技能、流失時整套廢掉。</p>
<h2 id="主流開源工具對照">主流開源工具對照</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>腳本</th>
          <th>規模</th>
          <th>學習曲線</th>
          <th>適用情境</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>k6</td>
          <td>JS</td>
          <td>中</td>
          <td>低-中</td>
          <td>複雜 user journey + CI 整合、現代工具首選</td>
      </tr>
      <tr>
          <td>JMeter</td>
          <td>XML/GUI</td>
          <td>中</td>
          <td>中-高</td>
          <td>企業已有流程、protocol 廣、reluctant 改</td>
      </tr>
      <tr>
          <td>Gatling</td>
          <td>Scala</td>
          <td>高</td>
          <td>高</td>
          <td>報表精美、Scala 學習門檻</td>
      </tr>
      <tr>
          <td>Locust</td>
          <td>Python</td>
          <td>高</td>
          <td>中</td>
          <td>複雜邏輯、Python 生態、單機 throughput 受限</td>
      </tr>
      <tr>
          <td>Vegeta</td>
          <td>CLI</td>
          <td>中</td>
          <td>低</td>
          <td>CLI driven、quick HTTP 壓測</td>
      </tr>
      <tr>
          <td>wrk/wrk2</td>
          <td>C</td>
          <td>高</td>
          <td>低</td>
          <td>單機極限 RPS、saturation discovery 用</td>
      </tr>
  </tbody>
</table>
<p><strong>k6</strong> 是過去 5 年崛起的綜合首選。JavaScript 腳本（前端工程師也能寫）、原生 dashboard、Prometheus exposition、CI 友善。Grafana 收購後生態加速。缺點：複雜 stateful 場景（DB connection pool 共享）需要繞 workaround。</p>
<p><strong>JMeter</strong> 是企業常見的 incumbent。協議支援廣（含 LDAP、JDBC、JMS）、有 GUI 編輯器。缺點：腳本是 XML、版本控制困難；GUI 主要用來生成腳本、實際跑壓測還是要 headless。已經在用的團隊建議繼續、新團隊不必特意選它。</p>
<p><strong>Gatling</strong> 高 throughput 純 async、性能優秀、報表精美。缺點：Scala / Kotlin DSL 學習曲線陡、新版本（11+）改了 DSL 不向後相容。</p>
<p><strong>Locust</strong> 是 Python 生態的選擇、特別適合複雜業務邏輯（用 Python 寫 user journey 自然）。分散式部署原生支援。缺點：Python 單線程 throughput 受限、要靠分散式擴容。</p>
<p><strong>Vegeta</strong> 跟 <strong>wrk</strong> 是「quick check」工具、用於單一 endpoint 的極限測試。不適合複雜場景、適合 saturation discovery 第一輪「找這個服務的天花板」。</p>
<h2 id="production-traffic-replay-工具">Production traffic replay 工具</h2>
<p>當需要複製 <em>真實 production traffic</em> 的壓測場景時、需要另一類工具。</p>
<p><strong>GoReplay</strong> 是最常用的開源 traffic replay 工具。在 production server 上 tcpdump-based 捕獲 HTTP traffic、可以 store 到 file 或 stream 到 staging 環境。優點：開源、無 vendor lock-in；缺點：HTTP only、加密流量要拿到 key 才能用。</p>
<p><strong>Service mesh shadow（Istio / Linkerd mirror）</strong>：mesh 層 mirror traffic 到 staging service。優點：mesh 已部署的話 zero infra cost、加密 traffic 也能 mirror。缺點：需要 service mesh 已落地。</p>
<p><strong>AWS VPC Traffic Mirroring</strong>：底層網路層 mirror、application 完全無感。優點：最低 invasion；缺點：AWS only、加密 traffic 要另外處理。</p>
<p><strong>Diffy（Twitter / X 開源、已 deprecated 但概念仍有效）</strong>：dual-write 同時打到舊 / 新版本、比對結果。適合驗證「新版本是否邏輯正確」、不是純壓測。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — 用分散式 EC2 跑 synthetic load 模擬 100K 同時搶票；<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">SeatGeek Virtual Waiting Room</a> — token 配發邏輯通常用 dual-write 驗證新舊版本一致。</p>
<h2 id="雲端-managed-壓測服務">雲端 managed 壓測服務</h2>
<p>當不想養 load test infrastructure、想 ad-hoc 跑大規模壓測時、用 managed service。</p>
<p><strong>AWS Distributed Load Testing</strong>：CloudFormation 起 Fargate cluster 跑 JMeter 或 Taurus、報表寫到 S3。優點：一鍵部署、Fargate 計費；缺點：JMeter-based、不是現代 k6 風格。</p>
<p><strong>Grafana k6 Cloud</strong>：託管 k6、跨地理 distributed 壓測（從多個 region 同時發流量）。優點：地理分散原生、跟 Grafana 整合無縫；缺點：vendor cost。</p>
<p><strong>Azure Load Testing</strong>：Azure 原生、整合 Application Insights。優點：Azure 用戶無縫；缺點：相對較新、生態還在補。</p>
<p><strong>GCP 沒有 first-party managed load testing</strong>：要靠 Marketplace 方案或自管 Locust on GKE。</p>
<h2 id="工具選型決策樹">工具選型決策樹</h2>
<p>落地時的快速決策：</p>
<ul>
<li>想快速驗證單一 API 極限 → wrk / Vegeta</li>
<li>想寫複雜 user journey + CI 整合 + JavaScript 團隊 → <strong>k6</strong>（新項目首選）</li>
<li>企業已有 JMeter 流程、不想換 → JMeter（接受 XML / GUI 複雜度）</li>
<li>大規模分散式 + Python 生態 → Locust</li>
<li>報表給管理層看、Scala 團隊 → Gatling</li>
<li>想複製真實 production traffic → GoReplay 或 service mesh shadow</li>
<li>想 ad-hoc 雲端大規模壓測 → 對應雲商的 managed load test</li>
</ul>
<h2 id="常見反模式">常見反模式</h2>
<ul>
<li><strong>只測單一 API、不測 user journey</strong>：找不到 cross-endpoint contention、找不到 session state 累積</li>
<li><strong>壓測機跟被測機在同一網段</strong>：網路延遲被低估、p99 比 production 樂觀</li>
<li><strong>壓測時 throttle 自己的工具</strong>：結果不是被測系統的極限、是工具自己的極限</li>
<li><strong>結果報表只看平均</strong>：<a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">tail latency</a> 看不到、p99 退化被掩蓋</li>
<li><strong>壓測環境跟 production hardware 不一致</strong>：CPU 型號、network、disk IOPS 差很大、結果不可外推</li>
<li><strong>沒驗證 model</strong>：跑了壓測但沒對比 production metrics、不知道 model 是否貼近 reality</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>10,000 台 t2.micro 跑分散式壓測（$130 / 小時）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>ML p99 &lt; 10ms 壓測必須帶 latency distribution</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（用工具找 knee）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a>（CI 整合）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/ci-pipeline/" data-link-title="6.1 CI pipeline" data-link-desc="CI pipeline 的分層策略、artifact 管理、flaky 治理與 release gate 輸入">06.1 CI Pipeline</a>（壓測在 CI 的位置）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/load-test/" data-link-title="Load Test" data-link-desc="說明在預期流量下驗證容量、延遲與降級策略的測試">Load Test</a></li>
<li><a href="/blog/backend/knowledge-cards/workload-model/" data-link-title="Workload Model" data-link-desc="描述 production traffic 形狀的可重播模型 — 容量規劃跟壓測的共同輸入">Workload Model</a></li>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
</ul>
]]></content:encoded></item><item><title>9.4 Saturation Discovery</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Saturation discovery 的責任是把「系統能撐多少」這個問題變成可量化答案。沒有 saturation 量測時、容量規劃只能猜；有 saturation 量測之後、能說「在當前配置下、p99 &amp;lt; 100ms 的條件下、能撐 X RPS、headroom Y%」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&amp;#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論&lt;/a> 的關係：9.1 預測 saturation curve 的形狀（linear → knee → cliff）、9.4 用實測找出 &lt;em>本服務&lt;/em> 的曲線具體位置。理論告訴我們 knee 存在、實測告訴我們它在哪裡。&lt;/p>
&lt;p>本章不深入工具操作（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3&lt;/a> 處理工具）、聚焦在 &lt;em>方法論&lt;/em> — 怎麼設計 ramp-up、怎麼判斷 knee、怎麼把結果文件化讓後續決策可用。&lt;/p>
&lt;h2 id="saturation-的精確定義">Saturation 的精確定義&lt;/h2>
&lt;p>容量規劃裡 saturation 不是「系統當機」、是「系統 &lt;em>進入 latency 指數成長區&lt;/em>」。這個區分很重要 — 系統 &lt;em>看起來&lt;/em> 還在跑、其實已經不可預測。&lt;/p>
&lt;p>技術上 saturation 對應 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">queueing theory 的 knee point&lt;/a>：utilization 超過某個臨界（M/M/c 通常 70-80%）、平均 queue length 從線性轉成指數成長。latency 是 queue length 的線性函數、所以也跟著指數成長。&lt;/p>
&lt;p>實務上把 saturation 分三段：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>linear region&lt;/strong>（utilization &amp;lt; 50%）：latency 平穩、加流量幾乎不影響&lt;/li>
&lt;li>&lt;strong>knee region&lt;/strong>（utilization 50-80%）：latency 開始上升、但還可接受&lt;/li>
&lt;li>&lt;strong>cliff region&lt;/strong>（utilization &amp;gt; 80%）：latency 不可預測、可能 timeout / cascade failure&lt;/li>
&lt;/ul>
&lt;p>健康系統運轉在 linear 後半段或 knee 前段（utilization 50-70%）、留出 headroom 應付 burst。autoscaler 的 target metric 通常訂在 60-70%、是這條曲線推導出的安全位置。&lt;/p>
&lt;h2 id="ramp-up-測試方法">Ramp-up 測試方法&lt;/h2>
&lt;p>要找出 saturation 點、必須跑 &lt;em>ramp-up 測試&lt;/em> — 不能固定一個壓力值。&lt;/p>
&lt;p>&lt;strong>單點壓測的問題&lt;/strong>：跑「2000 RPS 連續 10 分鐘」、看 latency 100ms、結論「能撐 2000 RPS」 — 但不知道 1500 跟 2500 RPS 是什麼樣。可能 1500 也是 100ms（離 knee 還很遠）、可能 2500 直接崩（已經在 cliff）。&lt;/p>
&lt;p>&lt;strong>Ramp-up 流程&lt;/strong>：從基線開始、按倍數加壓（1x / 2x / 4x / 8x &amp;hellip;）。每個壓力 level 維持 5-10 分鐘、觀察 latency / throughput / resource utilization 的穩態（不是 transient）。紀錄每個 level 的 percentile 分布。&lt;/p>
&lt;p>&lt;strong>Knee 出現的訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>throughput 從線性成長轉成 sub-linear（加壓但 throughput 不再等比成長）&lt;/li>
&lt;li>latency p50 還算穩、但 p99 / p999 開始飆&lt;/li>
&lt;li>resource saturation queue 開始堆積（不只 utilization 上升）&lt;/li>
&lt;li>error rate 仍接近 0（cliff 才會 error 飆）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Cliff 出現的訊號&lt;/strong>：throughput 開始下降（加壓反而越來越慢）、latency p99 變成 timeout、error rate 飆升、retry storm 出現。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Saturation discovery 的責任是把「系統能撐多少」這個問題變成可量化答案。沒有 saturation 量測時、容量規劃只能猜；有 saturation 量測之後、能說「在當前配置下、p99 &lt; 100ms 的條件下、能撐 X RPS、headroom Y%」。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> 的關係：9.1 預測 saturation curve 的形狀（linear → knee → cliff）、9.4 用實測找出 <em>本服務</em> 的曲線具體位置。理論告訴我們 knee 存在、實測告訴我們它在哪裡。</p>
<p>本章不深入工具操作（<a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3</a> 處理工具）、聚焦在 <em>方法論</em> — 怎麼設計 ramp-up、怎麼判斷 knee、怎麼把結果文件化讓後續決策可用。</p>
<h2 id="saturation-的精確定義">Saturation 的精確定義</h2>
<p>容量規劃裡 saturation 不是「系統當機」、是「系統 <em>進入 latency 指數成長區</em>」。這個區分很重要 — 系統 <em>看起來</em> 還在跑、其實已經不可預測。</p>
<p>技術上 saturation 對應 <a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">queueing theory 的 knee point</a>：utilization 超過某個臨界（M/M/c 通常 70-80%）、平均 queue length 從線性轉成指數成長。latency 是 queue length 的線性函數、所以也跟著指數成長。</p>
<p>實務上把 saturation 分三段：</p>
<ul>
<li><strong>linear region</strong>（utilization &lt; 50%）：latency 平穩、加流量幾乎不影響</li>
<li><strong>knee region</strong>（utilization 50-80%）：latency 開始上升、但還可接受</li>
<li><strong>cliff region</strong>（utilization &gt; 80%）：latency 不可預測、可能 timeout / cascade failure</li>
</ul>
<p>健康系統運轉在 linear 後半段或 knee 前段（utilization 50-70%）、留出 headroom 應付 burst。autoscaler 的 target metric 通常訂在 60-70%、是這條曲線推導出的安全位置。</p>
<h2 id="ramp-up-測試方法">Ramp-up 測試方法</h2>
<p>要找出 saturation 點、必須跑 <em>ramp-up 測試</em> — 不能固定一個壓力值。</p>
<p><strong>單點壓測的問題</strong>：跑「2000 RPS 連續 10 分鐘」、看 latency 100ms、結論「能撐 2000 RPS」 — 但不知道 1500 跟 2500 RPS 是什麼樣。可能 1500 也是 100ms（離 knee 還很遠）、可能 2500 直接崩（已經在 cliff）。</p>
<p><strong>Ramp-up 流程</strong>：從基線開始、按倍數加壓（1x / 2x / 4x / 8x &hellip;）。每個壓力 level 維持 5-10 分鐘、觀察 latency / throughput / resource utilization 的穩態（不是 transient）。紀錄每個 level 的 percentile 分布。</p>
<p><strong>Knee 出現的訊號</strong>：</p>
<ul>
<li>throughput 從線性成長轉成 sub-linear（加壓但 throughput 不再等比成長）</li>
<li>latency p50 還算穩、但 p99 / p999 開始飆</li>
<li>resource saturation queue 開始堆積（不只 utilization 上升）</li>
<li>error rate 仍接近 0（cliff 才會 error 飆）</li>
</ul>
<p><strong>Cliff 出現的訊號</strong>：throughput 開始下降（加壓反而越來越慢）、latency p99 變成 timeout、error rate 飆升、retry storm 出現。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 用 10K t2.micro 壓測</a> 找 DynamoDB 從 20 IOPS 到 135K 的擴展曲線、知道 knee 在哪。</p>
<h2 id="resource-saturation-的六個維度">Resource saturation 的六個維度</h2>
<p>每次 ramp-up 都要同時觀察六個維度的 resource saturation、找出哪個 <em>先 saturate</em>。</p>
<p><strong>CPU</strong>：utilization 100% <em>不一定</em> 等於 saturation。要看 load average 跟 run queue。utilization 80% 但 run queue 不斷增長 → 已 saturate；utilization 100% 但 run queue 空 → 還能撐（單純 CPU bound）。</p>
<p><strong>Memory</strong>：not OOM 即可？不夠。GC pause（Java、Go）、swap（Linux）、cache eviction 都是隱性 saturation。記憶體不直接 OOM 但 GC 飆 → 已影響 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">tail latency</a>。</p>
<p><strong>Disk I/O</strong>：要看三個維度：throughput（MB/s）、IOPS（operations/sec）、queue depth。雲端 SSD 通常先 IOPS bound、不是 throughput；本機 NVMe 可能先 throughput bound。</p>
<p><strong>Network</strong>：bandwidth（Gbps）、packets per second、connection count。雲端 instance 通常有 PPS limit、超過會 silent drop、不是顯式錯誤。</p>
<p><strong>Connection pool</strong>：DB / cache / external API 的連線數。這是 <em>最常見的隱性 bottleneck</em>。pool size 訂 100、實際在用 95 → utilization 看似還好、其實已經 saturate（剩下的 request 在等 connection）。</p>
<p><strong>External API quota</strong>：第三方 rate limit（Stripe、Twilio、Slack API）。這個維度的 saturation 看不到 <em>本系統</em> 的訊號、要看 <em>對方 API 的 429 error rate</em>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino RDB connection limit</a> — connection 是 RDB 的 saturation 點、CPU 跟 RAM 都還沒到。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片</a>。</p>
<h2 id="hot-partition-的隱性-saturation">Hot partition 的隱性 saturation</h2>
<p>對分散式 KV / OLTP（DynamoDB、Cosmos DB、Bigtable、Cassandra）、saturation 還有另一個維度：<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot partition</a>。</p>
<p>名義容量 = 每 partition 上限 × partition 數量。partition key 分布不均 → 名義容量達不到。整體 utilization 看起來 20% → 系統還能撐？不一定。最熱 partition 已經 100%、其他 partition 0%、整體平均才 20%、但加流量會打在最熱 partition、立即 throttle。</p>
<p><strong>識別 hot partition 的訊號</strong>：</p>
<ul>
<li>throughput 上不去、但 average resource utilization 低</li>
<li>某些 key 的 request latency 飆、其他 key 正常</li>
<li>DynamoDB throttling event 出現（即使 capacity 還沒滿）</li>
</ul>
<p><strong>處理方法</strong>：</p>
<ul>
<li>composite key（event_id + user_id_hash）</li>
<li>write sharding（event_id + random_suffix）</li>
<li>time-bucket（event_id + minute）</li>
<li>用 cache 吸收 hot key（DAX、ElastiCache）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 9000 萬 RPS</a> — partition 設計均勻時可以撐 sustained 高吞吐；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 售票</a> — 同一場演唱會（event_id）天然容易 hot、必須用 composite key 分散。</p>
<h2 id="long-tail-latency-的-saturation">Long-tail latency 的 saturation</h2>
<p>p50 / p95 / p99 / p999 在 saturate 時表現可能完全不同。</p>
<p>p50（中位數）對 GC pause、retry storm、tail latency 不敏感 — 大部分 request 沒事、p50 看不到。
p99（百分之 1）對 connection contention 開始敏感、能早期看到 saturation。
p999（千分之 1）對 GC stop-the-world、leader election、retry storm 敏感、是長尾的最強訊號。</p>
<p>純看 average / p50 會誤判 saturation 還沒到。SLO 通常訂 p99（讓 99% 用戶體驗良好）、internal critical 系統可訂 p99.9（5 個 9 的可用性對應 5 個 9 的 latency 期待）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML 系統的 user-perceived latency 是 <em>最後完成的 inference</em>、p50 快沒用；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — RAFT 系統的 p999 通常比 p99 高一個量級。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency 卡片</a>。</p>
<h2 id="saturation-文件化容量地圖">Saturation 文件化：容量地圖</h2>
<p>Saturation discovery 跑完之後、產出 <em>容量地圖</em> — 不是一個數字、是一張表。</p>
<p>容量地圖至少要回答：</p>
<ul>
<li>在 X 配置下（instance count、type、network）</li>
<li>SLO 條件 Y 下（p99 &lt; N ms、error rate &lt; M%）</li>
<li>能撐 Z RPS（含分解到不同 endpoint）</li>
<li>knee 在哪（什麼條件下進入 cliff）</li>
<li>第一個 saturate 的 resource 是什麼</li>
</ul>
<p>紀錄 <em>測試時間</em> 跟 <em>軟硬體版本</em>：硬體 / 軟體版本變動後、saturation 點可能位移、舊地圖不能套用。</p>
<p>加入 release gate：每次重大改動後 re-test、確認 knee 沒往不好的方向移。這層自動化跟 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 對接。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>DynamoDB IOPS 20 → 135K 的擴展曲線量測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads</a></td>
          <td>partition 均勻時的線性擴展</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>connection limit 是 RDB 的 saturation 點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a></td>
          <td>p99 &lt; 10ms saturation 條件比平均嚴格</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a> / <a href="/blog/backend/09-performance-capacity/load-test-tooling/" data-link-title="9.3 壓測工具選型" data-link-desc="k6 / JMeter / Gatling / Locust / Vegeta / Production Replay 的工程選型">9.3 壓測工具選型</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>（找到 knee 之後、定位是哪個 resource）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>（用 knee 算 headroom）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（量測訊號）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/saturation-point/" data-link-title="Saturation Point" data-link-desc="說明系統從線性穩態進入 latency 指數成長區的關鍵流量點">Saturation Point</a></li>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
<li><a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">Hot Partition</a></li>
</ul>
]]></content:encoded></item><item><title>9.5 瓶頸定位流程</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/bottleneck-localization/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>瓶頸定位的責任是回答「為什麼擴 app 沒用」這類問題。當 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 找到 knee point 之後、下一步是知道 &lt;em>哪個 resource&lt;/em> 先 saturate。沒有定位、容量規劃只能 &lt;em>全部翻倍&lt;/em>；有定位、可以 &lt;em>精準加在瓶頸層&lt;/em>。&lt;/p>
&lt;p>跟其他章節的關係：跟 9.4 是姊妹章（9.4 找出 knee、9.5 定位 knee 的成因）、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性&lt;/a> 互補（9.8 訊號治理、9.5 用訊號做定位）。&lt;/p>
&lt;p>本章不深入工具操作、聚焦在 &lt;em>方法論&lt;/em> — 怎麼按層次定位、怎麼避免常見誤判、怎麼區分可分散 vs 不可分散瓶頸。&lt;/p>
&lt;h2 id="use-methodresource-oriented-觀察">USE method：resource-oriented 觀察&lt;/h2>
&lt;p>Brendan Gregg 的 USE method 提供逐層定位的最小框架：對每個資源、量三個維度。&lt;/p>
&lt;p>&lt;strong>Utilization&lt;/strong>：資源使用率 0-100%。CPU 70%、memory 60%、disk 40% 這類數字。
&lt;strong>Saturation&lt;/strong>：資源排隊量（queue depth）。CPU run queue length、memory swap rate、disk I/O wait queue、connection pool wait count。
&lt;strong>Errors&lt;/strong>：資源層錯誤。CPU page fault、memory OOM、disk I/O error、network packet drop、connection refused。&lt;/p>
&lt;p>對每個資源（CPU / RAM / disk / network / DB connection / cache connection / file descriptor）逐一檢查。&lt;em>第一個出現 saturation 上升的資源是 bottleneck&lt;/em>、不是 utilization 最高的那個。&lt;/p>
&lt;p>USE 跟 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method&lt;/a>（rate / errors / duration）互補：USE 看「哪個資源頂不住」、RED 看「哪個 endpoint 表現變差」。容量規劃通常先用 USE 找瓶頸、再用 RED 看影響面。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片&lt;/a>。&lt;/p>
&lt;h2 id="逐層定位流程">逐層定位流程&lt;/h2>
&lt;p>從 application 層往下查、按依賴鏈逐層檢查。多數 bottleneck 在 application 跟 DB 兩層、但不能跳過其他層 — 偶爾真的在意外位置。&lt;/p>
&lt;p>&lt;strong>1. 應用層（application）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>thread / coroutine pool 使用率：是否已飽和&lt;/li>
&lt;li>event loop lag（Node.js、async runtime）：&amp;gt; 50ms 是警訊&lt;/li>
&lt;li>GC pause 頻率與時長：影響 p99 / p999&lt;/li>
&lt;li>request queue（accept queue、application internal queue）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. DB 層&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>connection pool 使用率（最常見隱性 bottleneck）&lt;/li>
&lt;li>slow query frequency&lt;/li>
&lt;li>replication lag&lt;/li>
&lt;li>lock contention（row lock、table lock）&lt;/li>
&lt;li>transaction queue depth&lt;/li>
&lt;/ul>
&lt;p>定位到 DB 層瓶頸時、優先檢查 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&amp;#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式&lt;/a> 清單 — 多數 DB 層瓶頸的根因是「應用程式發給 DB 的 query 寫法」、不是 DB 規格不夠。N+1 query 放大 connection 占用、long-running transaction 放大 lock contention、缺索引讓 slow query frequency 升高、&lt;code>SELECT *&lt;/code> 放大 transaction queue。這層判讀走完、再考慮 DB 規格升級或加 replica。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>瓶頸定位的責任是回答「為什麼擴 app 沒用」這類問題。當 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 找到 knee point 之後、下一步是知道 <em>哪個 resource</em> 先 saturate。沒有定位、容量規劃只能 <em>全部翻倍</em>；有定位、可以 <em>精準加在瓶頸層</em>。</p>
<p>跟其他章節的關係：跟 9.4 是姊妹章（9.4 找出 knee、9.5 定位 knee 的成因）、跟 <a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a> 互補（9.8 訊號治理、9.5 用訊號做定位）。</p>
<p>本章不深入工具操作、聚焦在 <em>方法論</em> — 怎麼按層次定位、怎麼避免常見誤判、怎麼區分可分散 vs 不可分散瓶頸。</p>
<h2 id="use-methodresource-oriented-觀察">USE method：resource-oriented 觀察</h2>
<p>Brendan Gregg 的 USE method 提供逐層定位的最小框架：對每個資源、量三個維度。</p>
<p><strong>Utilization</strong>：資源使用率 0-100%。CPU 70%、memory 60%、disk 40% 這類數字。
<strong>Saturation</strong>：資源排隊量（queue depth）。CPU run queue length、memory swap rate、disk I/O wait queue、connection pool wait count。
<strong>Errors</strong>：資源層錯誤。CPU page fault、memory OOM、disk I/O error、network packet drop、connection refused。</p>
<p>對每個資源（CPU / RAM / disk / network / DB connection / cache connection / file descriptor）逐一檢查。<em>第一個出現 saturation 上升的資源是 bottleneck</em>、不是 utilization 最高的那個。</p>
<p>USE 跟 <a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a>（rate / errors / duration）互補：USE 看「哪個資源頂不住」、RED 看「哪個 endpoint 表現變差」。容量規劃通常先用 USE 找瓶頸、再用 RED 看影響面。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method 卡片</a>。</p>
<h2 id="逐層定位流程">逐層定位流程</h2>
<p>從 application 層往下查、按依賴鏈逐層檢查。多數 bottleneck 在 application 跟 DB 兩層、但不能跳過其他層 — 偶爾真的在意外位置。</p>
<p><strong>1. 應用層（application）</strong>：</p>
<ul>
<li>thread / coroutine pool 使用率：是否已飽和</li>
<li>event loop lag（Node.js、async runtime）：&gt; 50ms 是警訊</li>
<li>GC pause 頻率與時長：影響 p99 / p999</li>
<li>request queue（accept queue、application internal queue）</li>
</ul>
<p><strong>2. DB 層</strong>：</p>
<ul>
<li>connection pool 使用率（最常見隱性 bottleneck）</li>
<li>slow query frequency</li>
<li>replication lag</li>
<li>lock contention（row lock、table lock）</li>
<li>transaction queue depth</li>
</ul>
<p>定位到 DB 層瓶頸時、優先檢查 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式</a> 清單 — 多數 DB 層瓶頸的根因是「應用程式發給 DB 的 query 寫法」、不是 DB 規格不夠。N+1 query 放大 connection 占用、long-running transaction 放大 lock contention、缺索引讓 slow query frequency 升高、<code>SELECT *</code> 放大 transaction queue。這層判讀走完、再考慮 DB 規格升級或加 replica。</p>
<p><strong>3. Cache 層</strong>：</p>
<ul>
<li>hit rate（突然下降是訊號）</li>
<li>eviction rate</li>
<li>connection 飽和（cache pool 也會耗盡）</li>
<li>memory utilization</li>
</ul>
<p><strong>4. Broker / queue 層</strong>：</p>
<ul>
<li>consumer lag（最重要的單一指標）</li>
<li>queue depth</li>
<li>dead-letter rate</li>
<li>broker connection count</li>
</ul>
<p><strong>5. 外部 API / 第三方 quota</strong>：</p>
<ul>
<li>rate limit 觸發頻率</li>
<li>retry storm（自家 retry 把對方 quota 打爆）</li>
<li>circuit breaker trip</li>
<li>timeout rate</li>
</ul>
<p><strong>6. 網路層</strong>：</p>
<ul>
<li>bandwidth utilization</li>
<li>packets per second（PPS limit）</li>
<li>socket count（file descriptor limit）</li>
<li>跨 region / 跨 AZ latency</li>
</ul>
<p><strong>7. DNS / load balancer</strong>：</p>
<ul>
<li>DNS resolution latency</li>
<li>LB connection establishment time</li>
<li>TLS handshake duration</li>
<li>backend health check failure</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino</a> RDB connection limit 是隱性 bottleneck、CPU / RAM 都還行；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 付款層獨立</a> — 把高頻搶票流量跟低頻付款流量分離、避免一層拖累另一層。</p>
<h2 id="profile-工具鏈">Profile 工具鏈</h2>
<p>USE 找出哪一層 saturate 之後、profile 工具找出 <em>該層的哪段 code</em> 拖累。</p>
<p><strong>Continuous profiling</strong>：Datadog Continuous Profiler、Pyroscope（開源 + Grafana 整合）、Parca（CNCF）、GCP Cloud Profiler、Azure Application Insights Profiler、AWS CodeGuru Profiler。production 持續取樣 CPU / heap / lock、overhead 通常 &lt; 1%。</p>
<p><strong>Distributed tracing</strong>：OpenTelemetry、Jaeger、Tempo、AWS X-Ray、GCP Cloud Trace、Azure Application Insights。記錄 request 在每個 service / 每個 stage 花了多少時間、找跨服務的 latency 累積。</p>
<p><strong>Flame graph</strong>：profile 結果視覺化的標準。從寬度可以看到「哪段 code 佔 CPU 最多」。學會看 flame graph 是 SRE 的基本功。</p>
<p><strong>Profile diff</strong>：壓測 baseline 跟 release candidate 比 stack 差異。看 <em>相對變化</em> 而非絕對值。詳見 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff 卡片</a>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora storage / compute 分離</a> — DB 統一後 application profile 變單純、退化來源更容易識別。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling 卡片</a>。</p>
<h2 id="跨層依賴鏈">跨層依賴鏈</h2>
<p>瓶頸不一定在 <em>本服務</em>、可能在 <em>下游服務</em>。這層判斷常被忽略。</p>
<p><strong>第三方 API quota</strong> 是常見隱性瓶頸。Twilio SMS、Stripe API、Slack webhook、Sendgrid email、Google Maps API 都有 rate limit。自家服務看起來健康、實際是 <em>對方 throttle</em>、自家 retry 再讓對方更慢。</p>
<p><strong>跨 region / 跨 zone 網路延遲</strong> 是累積的。一個 user request 經過 5 個 service、每個 service 跨 AZ 一次、累積 10-20ms cross-AZ latency。看起來每個 service 都很快、但 end-to-end 慢。</p>
<p><strong>Downstream cache</strong> 也是依賴。app 看起來健康、但其實是 cache 在擋；cache 突然 cold start（restart、eviction storm）、application 直接被打爆。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 行動支付</a> — DynamoDB 寫入可以撐 3K msg/sec、但 APNs / FCM 一天的 quota 有限、推送下游才是瓶頸。</p>
<h2 id="可分散-vs-不可分散瓶頸">可分散 vs 不可分散瓶頸</h2>
<p>定位完瓶頸後、要判斷它 <em>可不可以橫向擴</em>。這個判斷決定能不能用「加機器」解決。</p>
<p><strong>可分散瓶頸</strong>：</p>
<ul>
<li>stateless app server → 加機器有用</li>
<li>partitioned KV / OLTP（partition key 均勻時）→ 加 partition 有用</li>
<li>read replica（read-heavy workload）→ 加 replica 有用</li>
<li>worker pool → 加 worker 有用</li>
</ul>
<p><strong>不可分散瓶頸</strong>：</p>
<ul>
<li>consensus DB（RAFT / Paxos）→ 加節點不一定快（<a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> overhead）</li>
<li>single leader DB（master 寫）→ 必須垂直擴</li>
<li>中央 coordinator → 必須拆解或垂直擴</li>
<li>共享 cache（hot key）→ 必須改 partition key 或加 local cache</li>
</ul>
<p>判斷不可分散的關鍵是「協調成本」。一個操作必須 <em>跟所有 / 多數節點協調</em> 才能完成、就不可水平擴。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase RAFT consensus</a> — consensus 不可水平擴、所以 <em>選擇不擴</em>、改用單機極致；<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner TrueTime</a> — TrueTime 把協調成本 amortize 到 hardware（GPS + 原子鐘）、讓 OLTP 可水平擴。</p>
<h2 id="常見定位陷阱">常見定位陷阱</h2>
<p><strong>看單一指標就下結論</strong>：CPU 100% 不一定是 bottleneck（可能 saturation queue 空）；CPU 50% 不一定健康（可能 saturation queue 已滿）。always 看 USE 三個維度。</p>
<p><strong>平均看 OK、p99 看不出來</strong>：average latency 50ms 看起來健康、p99 500ms 已經出事。用 percentile、不用 average。</p>
<p><strong>Observer effect</strong>：profile / tracing 本身有 overhead、量測會輕微影響系統。critical path 上的 instrumentation 要 sampled 不要 100%。</p>
<p><strong>跨 release 比較 baseline 沒對齊</strong>：上週的 baseline 對應 v1.2、這週的 candidate 對應 v1.3、但 v1.2 跟 v1.3 之間還有 schema migration / hardware 變化、baseline 已經漂移。重新建 baseline 再 diff。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>connection limit 是 RDB 隱性 bottleneck</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 付款層獨立</a></td>
          <td>關鍵路徑切分避免 cross contamination</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase RAFT consensus</a></td>
          <td>不可分散 bottleneck</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">9.C26 PayPay</a></td>
          <td>下游 APNs / FCM quota 瓶頸</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>（針對 bottleneck 規劃）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a>（用 profile diff 改進）</li>
<li>下游：<a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a>（DB 層 bottleneck 多半在 query 寫法）</li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> / <a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff</a></li>
<li><a href="/blog/backend/knowledge-cards/universal-scalability-law/" data-link-title="Universal Scalability Law (USL)" data-link-desc="說明系統擴容到一定規模後吞吐反而下降的數學模型">Universal Scalability Law</a></li>
</ul>
]]></content:encoded></item><item><title>9.6 容量規劃模型</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>容量規劃的責任是把「未來 N 個月可能多大」翻成「現在該訂多少 capacity」。這層工作不純靠歷史外推、要結合業務 forecast、事件型成長、頂部風險 buffer。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的關係：9.4 提供「當前配置能撐多少」、9.6 用這個數字加上 forecast 推「該規劃多少」。沒有 9.4 的 baseline、9.6 只是猜；沒有 9.6 的 forecast、9.4 的 baseline 只是 snapshot。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸&lt;/a> 的關係：9.13 先決定「沿哪條軸擴」（垂直 / 水平 / Y 軸拆服務 / Z 軸 partition），9.6 才能算出「該擴多少」。同樣是「處理 10 倍流量」、選垂直擴展要算單機規格上限、選水平擴展要算協調成本跟連線池放大、選 Y 軸拆服務要算跨服務 latency budget — 三條軸的容量公式參數完全不同。沒先做 9.13、9.6 的數字會落到錯誤的擴展軸上。&lt;/p>
&lt;p>本章是「規劃決策」的章節、不是執行手冊。讀完後讀者能回答：peak 怎麼預測、headroom 訂多少、autoscaler 怎麼配、不可水平擴的服務怎麼處理。&lt;/p>
&lt;h2 id="容量公式三項">容量公式三項&lt;/h2>
&lt;p>容量規劃的核心公式可以濃縮成三項相乘：&lt;code>容量 = 預期峰值 × (1 + headroom) / 可擴容速度&lt;/code>。每一項都需要獨立分析：&lt;/p>
&lt;p>&lt;strong>預期峰值（peak forecast）&lt;/strong>：歷史 baseline × 預期成長 × 事件因子。三項中最影響整體準度。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast 卡片&lt;/a>。&lt;/p>
&lt;p>&lt;strong>Headroom budget&lt;/strong>：通常 30-50%、為了應付異常 burst + AZ 故障 + forecast 誤差。不同工作負載 headroom 不同。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &amp;#43; AZ 故障 &amp;#43; forecast 誤差的安全餘量">Headroom Budget 卡片&lt;/a>。&lt;/p>
&lt;p>&lt;strong>可擴容速度（reactive vs predictive）&lt;/strong>：autoscaler 反應時間 vs 流量上升速度。如果流量上升比 autoscaler 快、必須 &lt;em>提前&lt;/em> pre-scale、不能等 reactive 反應。&lt;/p>
&lt;p>這個公式的另一個寫法是「容量 = peak × 安全係數」、安全係數 = (1 + headroom) / 可擴容速度。預測準 + 擴容快 → 安全係數小、容量緊湊；預測差 + 擴容慢 → 安全係數大、成本高。&lt;/p>
&lt;h2 id="peak-forecast-方法">Peak forecast 方法&lt;/h2>
&lt;p>Forecast 方法分三層、按業務型態選用。&lt;/p>
&lt;p>&lt;strong>歷史線性外推&lt;/strong>：拿過去 N 個月的趨勢、按斜率外推到下 N 個月。適合 sustained growth（B2B SaaS 月增 X%）；不適合 event peak（年度活動）跟 surge（產品爆紅）。&lt;/p>
&lt;p>&lt;strong>季節性分解（STL：Seasonal-Trend decomposition using Loess）&lt;/strong>：把長期趨勢、週期成分、殘差分開預測。適合電商（雙 11 / Black Friday）、串流（IPL / Super Bowl）、零售（聖誕節）。需要 &lt;em>至少兩個完整 cycle&lt;/em> 的歷史資料。&lt;/p>
&lt;p>&lt;strong>業務 ML 模型&lt;/strong>：結合 marketing pipeline（廣告投入）、新用戶獲取（acquisition rate）、留存率、產品變化等多 feature。最精準但成本高、需要 ML team。&lt;/p>
&lt;p>&lt;strong>最常見錯誤是「拿去年同期 × (1 + 預期成長 %)」&lt;/strong>：忽略產品改動 + 行銷投入變化 + 外部事件。Prime Day 2025 vs 2024 不只是 +30% — 是 AI shopping assistant 上線、是 ad spend 變化、是新國家上線。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>容量規劃的責任是把「未來 N 個月可能多大」翻成「現在該訂多少 capacity」。這層工作不純靠歷史外推、要結合業務 forecast、事件型成長、頂部風險 buffer。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的關係：9.4 提供「當前配置能撐多少」、9.6 用這個數字加上 forecast 推「該規劃多少」。沒有 9.4 的 baseline、9.6 只是猜；沒有 9.6 的 forecast、9.4 的 baseline 只是 snapshot。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的關係：9.13 先決定「沿哪條軸擴」（垂直 / 水平 / Y 軸拆服務 / Z 軸 partition），9.6 才能算出「該擴多少」。同樣是「處理 10 倍流量」、選垂直擴展要算單機規格上限、選水平擴展要算協調成本跟連線池放大、選 Y 軸拆服務要算跨服務 latency budget — 三條軸的容量公式參數完全不同。沒先做 9.13、9.6 的數字會落到錯誤的擴展軸上。</p>
<p>本章是「規劃決策」的章節、不是執行手冊。讀完後讀者能回答：peak 怎麼預測、headroom 訂多少、autoscaler 怎麼配、不可水平擴的服務怎麼處理。</p>
<h2 id="容量公式三項">容量公式三項</h2>
<p>容量規劃的核心公式可以濃縮成三項相乘：<code>容量 = 預期峰值 × (1 + headroom) / 可擴容速度</code>。每一項都需要獨立分析：</p>
<p><strong>預期峰值（peak forecast）</strong>：歷史 baseline × 預期成長 × 事件因子。三項中最影響整體準度。詳見 <a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast 卡片</a>。</p>
<p><strong>Headroom budget</strong>：通常 30-50%、為了應付異常 burst + AZ 故障 + forecast 誤差。不同工作負載 headroom 不同。詳見 <a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget 卡片</a>。</p>
<p><strong>可擴容速度（reactive vs predictive）</strong>：autoscaler 反應時間 vs 流量上升速度。如果流量上升比 autoscaler 快、必須 <em>提前</em> pre-scale、不能等 reactive 反應。</p>
<p>這個公式的另一個寫法是「容量 = peak × 安全係數」、安全係數 = (1 + headroom) / 可擴容速度。預測準 + 擴容快 → 安全係數小、容量緊湊；預測差 + 擴容慢 → 安全係數大、成本高。</p>
<h2 id="peak-forecast-方法">Peak forecast 方法</h2>
<p>Forecast 方法分三層、按業務型態選用。</p>
<p><strong>歷史線性外推</strong>：拿過去 N 個月的趨勢、按斜率外推到下 N 個月。適合 sustained growth（B2B SaaS 月增 X%）；不適合 event peak（年度活動）跟 surge（產品爆紅）。</p>
<p><strong>季節性分解（STL：Seasonal-Trend decomposition using Loess）</strong>：把長期趨勢、週期成分、殘差分開預測。適合電商（雙 11 / Black Friday）、串流（IPL / Super Bowl）、零售（聖誕節）。需要 <em>至少兩個完整 cycle</em> 的歷史資料。</p>
<p><strong>業務 ML 模型</strong>：結合 marketing pipeline（廣告投入）、新用戶獲取（acquisition rate）、留存率、產品變化等多 feature。最精準但成本高、需要 ML team。</p>
<p><strong>最常見錯誤是「拿去年同期 × (1 + 預期成長 %)」</strong>：忽略產品改動 + 行銷投入變化 + 外部事件。Prime Day 2025 vs 2024 不只是 +30% — 是 AI shopping assistant 上線、是 ad spend 變化、是新國家上線。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day 年增率 +30% ~ +77%</a> — 連 Amazon 自家每年成長都不能線性外推；<a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 新片發布</a> — 事件型 forecast、按過去新片 metric 預估。</p>
<p>Forecast 必須有 <em>誤差範圍</em>、不能單一數字。給上下界（最壞 / 預期 / 最好）、容量規劃才能用 worst-case 訂 baseline。</p>
<h2 id="headroom-budget-設計">Headroom budget 設計</h2>
<p>Headroom 不是 over-provisioning 浪費、是容量規劃的安全邊界。常見比例 30-50%、按 saturation 行為跟工作負載敏感度調整。</p>
<p><strong>為什麼是 30-50% 而不是 10%</strong>：</p>
<ul>
<li>forecast 誤差：預測準度通常 ±20-30%</li>
<li>burst pattern：瞬間 spike 超過 average peak、需要短時間吸收</li>
<li>AZ / region failover：一個 AZ 掛、剩下兩個要承擔全部（多 33% 容量）</li>
<li>系統老化 / drift：軟硬體升級後 saturation 點可能位移</li>
</ul>
<p><strong>不同工作負載不同 headroom</strong>：</p>
<ul>
<li>stateless service：30%（autoscaler 反應快、headroom 可以薄）</li>
<li>DB：50%（不易擴容、要備援足夠空間）</li>
<li>broker / queue：60%（consumer 落後恢復時要瞬間吃下積壓）</li>
<li>consensus DB：80%+（完全不能 reactive 擴）</li>
</ul>
<p><strong>headroom 太低 → 出事</strong>：peak 期間進 cliff、用戶體驗變差。
<strong>headroom 太高 → 浪費錢</strong>：平日成本拉高、CFO 質疑。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech AI 預測</a> — 預測準了可以降 headroom 比例；預測不準必須拉高 headroom 補回安全邊界。</p>
<h2 id="growth-curve-形狀分類">Growth curve 形狀分類</h2>
<p>不同 growth curve 形狀對應不同 forecast 方法跟 review 節奏。</p>
<p><strong>Linear growth</strong>：用戶月增 X%。B2B SaaS 最常見。forecast 線性外推、每季 review、headroom 可以薄（成長可預測）。</p>
<p><strong>Step growth</strong>：每次行銷 / 活動跳一階、之間 plateau。需要 event tier 規劃、每個事件單獨 forecast、headroom 跟 event 強度連動。</p>
<p><strong>Exponential growth</strong>：早期初創、病毒擴散。forecast 容易低估、傳統線性外推會大幅低估；headroom 必須拉到 100%+、不能省。</p>
<p><strong>S-curve growth</strong>：成熟產品、會 saturate。Forecast 初期像 exponential、中期 plateau、晚期 mature。需要識別 inflection point、過了就調 forecast 方法。</p>
<p><strong>Cyclical</strong>：電商季節性。每年 Black Friday / Cyber Monday / Christmas / Chinese New Year 都重複、forecast 用 STL 季節性分解。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID</a> — step growth、外部衝擊讓 baseline 永久上移；<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO 50x surge</a> — exponential（早期）+ 之後 S-curve；<a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">ASOS Black Friday</a> — cyclical。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve 卡片</a>。</p>
<h2 id="autoscaling-sizing">Autoscaling sizing</h2>
<p>訂好 capacity 之後、要設計 autoscaler 把這個容量 <em>動態使用</em>。</p>
<p><strong>min / max / target metric 三個參數</strong>：</p>
<ul>
<li>min 太低 → cold start 風險（流量上來時還在 boot）</li>
<li>min 太高 → 平日浪費</li>
<li>max 太低 → 限流（peak 時 autoscaler 不能再擴）</li>
<li>max 太高 → 月底炸帳單（autoscaler 不受控、過 peak 不會主動降）</li>
<li>target 太高 → autoscale 啟動太晚、進 knee 才反應</li>
<li>target 太低 → autoscale 太敏感、頻繁 scale up / down 浪費</li>
</ul>
<p><strong>Predictive vs reactive</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>：根據歷史 pattern 或 ML 模型提前擴</li>
<li>reactive scaling：根據當下指標擴</li>
<li>兩者組合最穩：predictive 處理已知 pattern、reactive 處理 unexpected burst</li>
</ul>
<p><strong>Scheduled vs metric-based</strong>：</p>
<ul>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a>：時段觸發（年度活動、daily peak）</li>
<li>metric-based：根據 utilization / queue depth 觸發</li>
<li>三層組合（scheduled + predictive + reactive）最穩</li>
</ul>
<p><strong>不同層的 autoscaler 各自設計</strong>：</p>
<ul>
<li>EC2 Auto Scaling Group：infrastructure 層</li>
<li>Kubernetes HPA / VPA：pod 層</li>
<li>Karpenter：node 層</li>
<li>DynamoDB auto-scaling：DB capacity 層</li>
<li>CloudFront：CDN 層</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 30 分鐘擴 130 倍</a> — 6 台 → 800 台靠 ASG + AMI prebuild + ELB warmup；<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day predictive</a> — pre-scaling 30-77% 年增率提前算進容量。</p>
<h2 id="不可水平擴容服務的容量規劃">不可水平擴容服務的容量規劃</h2>
<p>部分服務不能用「加機器」解決容量問題。這類服務的容量規劃有獨立邏輯。</p>
<p><strong>典型不可水平擴</strong>：</p>
<ul>
<li>consensus DB（RAFT / Paxos）：節點數量是 consensus 一部分、不能臨時增減</li>
<li>single leader DB（PostgreSQL primary、MySQL master）：寫只有一個 leader</li>
<li>中央 coordinator：必須拆解才可擴</li>
</ul>
<p><strong>容量公式變成</strong>：單機極限 × headroom、沒有 elastic 救援。
<strong>設計重點</strong>：</p>
<ul>
<li>預先 provision 到能撐 peak、不依賴 reactive 擴</li>
<li>垂直擴容（更大 instance）為主、不是橫向</li>
<li>留更高 headroom（80%+）、出事沒有第二招</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase pre-provision</a> — RAFT 限制下完全 pre-provision、不 autoscale；<a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">Spanner 節點即容量單位</a> — 雖然全球可擴、但每個 region 內節點數要預先規劃。</p>
<h2 id="跨地理--跨-region-容量規劃">跨地理 / 跨 region 容量規劃</h2>
<p>跨 region 服務不能用 <em>全球總量</em> 平攤、每個 region 獨立規劃。</p>
<p><strong>為什麼不能聚合</strong>：</p>
<ul>
<li>用戶在哪、流量就在哪、不會自動 spread</li>
<li>跨 region 切流量有延遲（DNS TTL、用戶習慣）、不能即時 rebalance</li>
<li>資料駐留合規可能強制各 region 獨立</li>
</ul>
<p><strong>規劃方法</strong>：</p>
<ul>
<li>每個 region 抽各自的 workload model</li>
<li>各自跑 saturation discovery</li>
<li>各自訂 headroom（區域峰值 + 區域 AZ failover）</li>
<li>跨 region failover plan：哪個 region 掛了、流量去哪、目標 region 要留多少 headroom 接</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">Standard Chartered 7 個受監管市場</a> — 跨市場獨立容量規劃；<a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 15 region</a> — 15 主 region + 5 衛星 region 各自規劃；<a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">Mercado Libre 18 國</a> — 每國獨立 cycle。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a></td>
          <td>可預期峰值的 forecast + pre-scaling</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a></td>
          <td>AI 預測式擴容、縮短反應時間</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>30x surge 後 baseline 永久上移</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場獨立容量規劃</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>不可水平擴的 pre-provision</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">9.2 Workload Modeling</a> / <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a>（先選軸再算數量、不可水平擴容服務的判讀基底）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a>（容量翻成成本）</li>
<li>下游：<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>跨模組：<a href="/blog/backend/05-deployment-platform/" data-link-title="模組五：部署平台與網路入口" data-link-desc="整理 Kubernetes、systemd、load balancer、container 與服務生命週期合約">05 部署平台模組</a> autoscaler 實作</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/growth-curve/" data-link-title="Growth Curve" data-link-desc="說明用戶 / 流量隨時間成長的五種典型形狀、影響容量規劃方法">Growth Curve</a></li>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>9.7 成本邊界與 efficiency</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 &lt;em>在每一個容量決策點&lt;/em> 把「多保險」跟「多省錢」一起評估。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。&lt;/p>
&lt;p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。&lt;/p>
&lt;h2 id="cost-per-request-模型">Cost per request 模型&lt;/h2>
&lt;p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。&lt;/p>
&lt;p>&lt;strong>基本公式&lt;/strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
&lt;strong>分 stage 拆解&lt;/strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
&lt;strong>分 endpoint 拆解&lt;/strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。&lt;/p>
&lt;p>&lt;strong>對齊業務 metric&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>cost per active user：總成本 / MAU&lt;/li>
&lt;li>cost per transaction：總成本 / 完成的訂單數&lt;/li>
&lt;li>cost per ML inference：總成本 / inference 次數&lt;/li>
&lt;/ul>
&lt;p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降&lt;/a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降&lt;/a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片&lt;/a>。&lt;/p>
&lt;h2 id="cost-curve-形狀">Cost curve 形狀&lt;/h2>
&lt;p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。&lt;/p>
&lt;p>&lt;strong>On-demand（pay-per-use）&lt;/strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
&lt;strong>Reserved instances（RI）/ Savings Plans&lt;/strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
&lt;strong>Spot instances&lt;/strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 <em>在每一個容量決策點</em> 把「多保險」跟「多省錢」一起評估。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。</p>
<p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。</p>
<h2 id="cost-per-request-模型">Cost per request 模型</h2>
<p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。</p>
<p><strong>基本公式</strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
<strong>分 stage 拆解</strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
<strong>分 endpoint 拆解</strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。</p>
<p><strong>對齊業務 metric</strong>：</p>
<ul>
<li>cost per active user：總成本 / MAU</li>
<li>cost per transaction：總成本 / 完成的訂單數</li>
<li>cost per ML inference：總成本 / inference 次數</li>
</ul>
<p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降</a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降</a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片</a>。</p>
<h2 id="cost-curve-形狀">Cost curve 形狀</h2>
<p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。</p>
<p><strong>On-demand（pay-per-use）</strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
<strong>Reserved instances（RI）/ Savings Plans</strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
<strong>Spot instances</strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。</p>
<p><strong>最佳組合通常是「Reserved baseline + On-demand spike + Spot batch」</strong>：</p>
<ul>
<li>Reserved 覆蓋 baseline 容量（永遠用得到）</li>
<li>On-demand 處理 peak 跟 unpredicted burst</li>
<li>Spot 跑 batch 工作（不在 critical path、可被中斷）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">Riot Games 年省 1000 萬</a> — 從自管 Mesos 遷到 EKS、降的不只是 instance cost、是 cluster 管理人力 + ops 簡化；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 30% 成本下降</a> — DynamoDB + EKS 取代自管、釋放 DBA 人力。</p>
<h2 id="over-provisioning-vs-under-provisioning-取捨">Over-provisioning vs under-provisioning 取捨</h2>
<p>容量決策的核心經濟學問題：訂多大容量才是最划算？</p>
<p><strong>Over-provisioning 成本</strong>：每月多付 $X 雲端費。這個數字直接看帳單。
<strong>Under-provisioning 成本</strong>：sigma 機率 × downtime × revenue per minute。這個數字更難算 — 需要 historical incident rate + downtime impact analysis。</p>
<p><strong>兩個成本平衡點 = 經濟最佳 headroom</strong>。但實務上 under-provisioning 成本不容易量化、保守做法是把 sigma 機率拉高（用 worst-case 估）、headroom 訂寬一點。</p>
<p><strong>Critical workload</strong>（金融、醫療、付款）：under-provisioning 成本極高（合約違約 + 客戶流失 + 法規）、寧可 over-provisioning 30-50%。
<strong>Non-critical workload</strong>（內部工具、分析、batch）：under-provisioning 成本低、可以更貼近 minimum capacity。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato TiDB 必須 over-provision</a> — 為了應付 spike、TiDB 必須長期 over-provision；DynamoDB on-demand 不必、pay-per-use 自然處理。</p>
<h2 id="降級的成本邊界">降級的成本邊界</h2>
<p>「降級 vs 擴容」是常見容量決策、但常被當成「技術問題」而非「成本問題」。</p>
<p><strong>降級不是免費</strong>：</p>
<ul>
<li>流失轉換：UI 顯示「系統忙碌」、用戶可能放棄</li>
<li>客訴成本：客服處理客訴的 OpEx</li>
<li>品牌損失：社群媒體負面評論、口碑下降</li>
<li>合約違約：B2B 客戶可能基於 SLA 求償</li>
</ul>
<p><strong>算「降級 vs 擴容」哪個成本低</strong>：</p>
<ul>
<li>擴容成本：peak 時段多付的 cloud 費用</li>
<li>降級成本：上述四項合計</li>
<li>哪邊低就選哪邊</li>
</ul>
<p><strong>降級觸發條件</strong>通常按負載門檻 / 成本門檻 / SLA 觸發：</p>
<ul>
<li>負載門檻：utilization &gt; 85% → 啟動降級</li>
<li>成本門檻：本月雲端費已超預算 X% → 啟動降級</li>
<li>SLA 觸發：<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 快用完 → 啟動降級保 SLA</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO 50x surge</a> — surge 期間無法等比擴容、必須降級保住核心遊戲機制、犧牲附加功能。</p>
<h2 id="人力成本工程化">人力成本工程化</h2>
<p>雲端帳單是顯性成本、但 <em>人力成本</em> 是常被忽略的隱性容量成本。</p>
<p><strong>自建 vs managed 的人力成本對比</strong>：</p>
<ul>
<li>自建 Kafka / PostgreSQL / Redis：需要 DBA / SRE 持續維護 + 升級 + 故障處理</li>
<li>Managed 服務（MSK、Aurora、ElastiCache）：vendor 負責 patch、backup、failover</li>
<li>差距通常 <em>3-10 倍</em> 人力成本</li>
</ul>
<p><strong>DBA / SRE / network engineer 都是隱性容量成本</strong>：</p>
<ul>
<li>一個資深 DBA 在美國年薪 $200K+、台灣 NTD 200-400 萬</li>
<li>工程師時間是有上限的、自管系統佔的時間就是 <em>無法投入產品開發</em> 的機會成本</li>
</ul>
<p><strong>「90% 工程工時下降」是管理 ROI 的關鍵</strong>：重點是把工程資源從 <em>維持</em> 轉移到 <em>建構</em>、不是拿來吹噓技術。這條自建 vs managed 的人力成本對比、是 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a> 裡「計費隨規模成長、自建 TCO 出現交叉點」那條 tripwire 的算法側 — 選型方向在 0.22 判、成本量化在這裡做。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">Spotify Kafka → Pub/Sub</a> — 不是因為 Pub/Sub 便宜、是因為 Spotify 規模下自管 Kafka 的人力成本不划算；<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 90% 工程工時降</a> — managed 路線讓電信商級新串流服務只用 5-10 個工程師 launch；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom DBA 釋放</a> — 把 DBA 時間從 patching 轉到遊戲品質。</p>
<h2 id="finops-跟容量規劃的整合">FinOps 跟容量規劃的整合</h2>
<p>FinOps 是 <em>財務跟工程的協作框架</em>、把成本決策從事後對帳變成事前規劃。</p>
<p><strong>Showback / chargeback</strong>：把雲端成本攤到團隊 / 服務 / feature。每個團隊看得到自己的成本、自然開始 optimize。chargeback（實際扣預算）比 showback（純展示）更有效但組織複雜度高。</p>
<p><strong>每月 cost review 變成容量 review 的一部分</strong>：</p>
<ul>
<li>對比預算 vs 實際</li>
<li>找出 top 5 cost driver</li>
<li>對比上月趨勢、看是否有 anomaly</li>
<li>跟 capacity team 一起討論 right-sizing</li>
</ul>
<p><strong>Spot diversification</strong>：spot 中斷風險可以靠 <em>多 instance type 跟多 AZ</em> 分散。例如：spot pool 同時包含 m5.large + m5a.large + m5n.large、各 AZ 都有、單一 type pool 撤回時其他 type 還在。</p>
<p><strong>Right-sizing</strong>：定期 review instance type 是否最適。常見浪費：訂太大 instance（CPU / RAM 用 30%）、過時 instance generation（用 c5 沒升到 c7）、reserved 過剩。</p>
<h2 id="反模式">反模式</h2>
<p>容量成本的常見錯誤模式：</p>
<p><strong>Autoscaling max 設無限大</strong>：流量爆衝時 autoscaler 跟著爆衝、月底帳單炸裂。max 必須訂、是 financial circuit breaker。</p>
<p><strong>全部用 on-demand、沒談 reserved / savings plan</strong>：cloud spending &gt; $10K/月 已經值得跟雲商 talk discount、savings plan 通常 30-60% off。</p>
<p><strong>沒成本 monitoring、直到帳單來才知道</strong>：要建 daily cost dashboard、anomaly 即時 alert、不要等月帳單。</p>
<p><strong>降級用人工觸發、出事時來不及</strong>：降級邏輯要 <em>自動化</em>、按 metric 觸發、不是 oncall 工程師看到 dashboard 才下指令。</p>
<p><strong>忘了人力成本</strong>：算 build vs buy 只算 cloud 費、忘了 SRE / DBA 時間、結果發現「省的 cloud 費 &lt; 多花的人力」。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>50% 成本下降（從 over-provision 解放）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a></td>
          <td>年省 1000 萬（EKS 替代 Mesos）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>28% 成本下降（DB consolidation）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>90% 工程工時降（managed 路線）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom</a></td>
          <td>30% 成本下降（DBA 釋放到遊戲品質）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a>（cost attribution）</li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.8 效能可觀測性</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/performance-observability/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery&lt;/a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組&lt;/a> 是 sibling — 04 處理通用觀測、9.8 處理 &lt;em>容量規劃用&lt;/em> 的觀測。&lt;/p>
&lt;p>本章不重複 04 的訊號治理基礎、聚焦在 &lt;em>容量 / 效能 / 成本三條觀測線怎麼整合&lt;/em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。&lt;/p>
&lt;h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method&lt;/a> 不只是壓測時用、production 也要持續監控。&lt;/p>
&lt;p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Utilization&lt;/strong>（使用率 0-100%）：直觀但會誤判&lt;/li>
&lt;li>&lt;strong>Saturation&lt;/strong>（queue depth）：早期警訊&lt;/li>
&lt;li>&lt;strong>Errors&lt;/strong>（資源層錯誤）：已經出事的訊號&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>為什麼不能只看 utilization&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）&lt;/li>
&lt;li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Saturation metric 是 capacity warning 的最早訊號&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>queue depth（每個 queue / pool）&lt;/li>
&lt;li>connection pool 使用率（最常見隱性 bottleneck）&lt;/li>
&lt;li>thread pool / coroutine count&lt;/li>
&lt;li>event loop lag（Node.js、async runtime）&lt;/li>
&lt;li>GC pause time / frequency&lt;/li>
&lt;li>cache hit rate / eviction rate&lt;/li>
&lt;li>replication lag&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Dashboard 設計&lt;/strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 &lt;em>saturation 起飛&lt;/em> 時觸發、不是 utilization 滿。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &amp;#43; AWS Media Services 撐 30 channels live &amp;#43; 5M MAU、工程工時下降 90%">Lemino connection limit&lt;/a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%&lt;/a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>效能可觀測性的責任是讓容量決策有訊號基礎。沒有適當訊號時、就算有壓測結果跟容量計畫、也看不到「現在實際距離 saturation 多遠」、無法做即時調整。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> 的關係：9.4 找到 saturation 點、9.8 定義持續監控這個點的訊號跟 dashboard。跟 <a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a> 是 sibling — 04 處理通用觀測、9.8 處理 <em>容量規劃用</em> 的觀測。</p>
<p>本章不重複 04 的訊號治理基礎、聚焦在 <em>容量 / 效能 / 成本三條觀測線怎麼整合</em>。讀完後讀者能設計一個「容量 dashboard」、回答「現在距離 saturation 還有多遠、什麼時候該擴」。</p>
<h2 id="use-method-在-production-持續監控">USE method 在 production 持續監控</h2>
<p><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE method</a> 不只是壓測時用、production 也要持續監控。</p>
<p>對每個資源（CPU / RAM / disk / network / DB connection / cache pool / file descriptor）量三個維度：</p>
<ul>
<li><strong>Utilization</strong>（使用率 0-100%）：直觀但會誤判</li>
<li><strong>Saturation</strong>（queue depth）：早期警訊</li>
<li><strong>Errors</strong>（資源層錯誤）：已經出事的訊號</li>
</ul>
<p><strong>為什麼不能只看 utilization</strong>：</p>
<ul>
<li>CPU 100% 但 run queue 空 → 還能撐（單純 CPU bound）</li>
<li>CPU 80% 但 run queue 不斷增長 → 已 saturate（saturation 比 utilization 領先）</li>
</ul>
<p><strong>Saturation metric 是 capacity warning 的最早訊號</strong>：</p>
<ul>
<li>queue depth（每個 queue / pool）</li>
<li>connection pool 使用率（最常見隱性 bottleneck）</li>
<li>thread pool / coroutine count</li>
<li>event loop lag（Node.js、async runtime）</li>
<li>GC pause time / frequency</li>
<li>cache hit rate / eviction rate</li>
<li>replication lag</li>
</ul>
<p><strong>Dashboard 設計</strong>：每個關鍵資源獨立 panel、同時顯示 utilization 跟 saturation。alert 在 <em>saturation 起飛</em> 時觸發、不是 utilization 滿。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino connection limit</a> — connection saturation 是 RDB 的真正 bottleneck、不是 CPU；<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato latency 降 90%</a> — 從 TiDB 換到 DynamoDB、saturation 行為完全不同、observability 也要跟著改。</p>
<h2 id="red-method請求層的容量訊號">RED method：請求層的容量訊號</h2>
<p><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED method</a> 跟 USE 互補、從請求層看容量。</p>
<ul>
<li><strong>Rate</strong>：requests per second（每個 service / endpoint）</li>
<li><strong>Errors</strong>：error rate</li>
<li><strong>Duration</strong>：latency distribution（histogram、不是單一 percentile）</li>
</ul>
<p><strong>Duration 比 Errors 早</strong>：duration p99 飆通常先於 error rate 上升、是 saturation 的早期警訊。</p>
<p><strong>每個 endpoint 都要有 RED</strong>：不能只看全站 average、要分 endpoint。登入 endpoint 跟結帳 endpoint 的 saturation 行為不同、混在一起看不到 issue。</p>
<p><strong>Histogram 是必須、不是 nice-to-have</strong>：</p>
<ul>
<li>只記 p99 → 看不到 p999、看不到 distribution shape</li>
<li>記 histogram → 可以隨時算任何 percentile、可以做 long-tail 分析</li>
<li>Prometheus histogram、OpenMetrics histogram 是現代標準</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech 25ms p95</a> — p95 是業務 KPI、不是技術指標、每個 endpoint 都有獨立 SLO。</p>
<h2 id="p50--p95--p99--p999-的取捨">p50 / p95 / p99 / p999 的取捨</h2>
<p>不同 percentile 反映不同問題、選錯 percentile 會錯失 issue。</p>
<ul>
<li><strong>p50（中位數）</strong>：整體狀況、感覺正常的指標、對長尾不敏感</li>
<li><strong>p95</strong>：日常 user-perceived experience、大多數用戶感受到的延遲</li>
<li><strong>p99</strong>：minority but critical 用戶體驗、SLO 常訂在這</li>
<li><strong>p999</strong>：極端長尾、受 GC pause / leader election / retry storm 影響、internal critical 系統訂在這</li>
</ul>
<p><strong>業務 SLO 通常訂 p99</strong>：「99% 用戶 request &lt; 500ms」是常見承諾、合約 SLA 也通常基於 p99。
<strong>Internal critical 系統訂 p99.9</strong>：金融交易、即時配對、客服 SaaS（5 個 9 可用性對應 5 個 9 latency 期待）。</p>
<p><strong>紀錄分布、不只紀錄 percentile</strong>：</p>
<ul>
<li>gauge p99 → 看不到 distribution shape、看不到 multimodal 分布</li>
<li>histogram → 可以重新計算任何 percentile、可以對比 distribution、可以找 anomaly</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML inference 在 p99 才能控制用戶體驗、p50 沒意義；<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — 必須關注 p999、RAFT 系統長尾顯著。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency 卡片</a>。</p>
<h2 id="cost-dashboard">Cost dashboard</h2>
<p>成本訊號跟容量訊號要 <em>並列顯示</em>、不要分開看。</p>
<p><strong>Per-service / per-endpoint cost attribution</strong>：</p>
<ul>
<li>每個 service 自己的雲端成本</li>
<li>拆到每個 endpoint</li>
<li>跟 RPS / latency 並列、看「成本上升是因為流量還是低效」</li>
</ul>
<p><strong>Cost per request 的時序變化</strong>：</p>
<ul>
<li>突然上升通常是 <em>退化</em> 訊號（新版本沒效率）</li>
<li>緩慢上升通常是 <em>規模</em> 訊號（用戶增加但 efficiency 沒變）</li>
</ul>
<p><strong>成本異常告警（vs 容量異常告警）</strong>：</p>
<ul>
<li>容量告警：utilization &gt; X% → 擴容</li>
<li>成本告警：cost spike &gt; X% → review</li>
<li>兩者可能同時觸發（autoscaler 擴容也擴 cost）、要區分</li>
</ul>
<p><strong>跟業務 metric 對齊</strong>：cost per active user、cost per transaction、cost per ML inference。業務 metric 級別的 cost 才能 review unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/lyft-microservice-eight-x-peak/" data-link-title="9.C7 Lyft：100&#43; 微服務在 8 倍峰值下的 Auto Scaling" data-link-desc="Lyft 用 AWS Auto Scaling 跨 100&#43; 個微服務承載 8 倍峰值流量、跨 200&#43; 城市">Lyft 100+ 微服務各自 cost</a> — 微服務粒度的 cost attribution、找出哪個 service 過貴；對應 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a>。</p>
<h2 id="continuous-profiling">Continuous profiling</h2>
<p><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous profiling</a> 是現代效能 observability 的關鍵環節 — production 持續取 profile（CPU / heap / lock）、隨時可以做 diff 跟 root cause。</p>
<p><strong>工具生態</strong>：</p>
<ul>
<li>Datadog Continuous Profiler、Pyroscope（開源 + Grafana 整合）、Parca（CNCF）</li>
<li>GCP Cloud Profiler、Azure Application Insights Profiler、AWS CodeGuru Profiler</li>
<li>Overhead 通常 &lt; 1% CPU、放心開在 production</li>
</ul>
<p><strong>跟 distributed tracing 整合</strong>：trace → span → profile。一個 slow request 點下去、能看到對應 span、再下去看 profile。</p>
<p><strong>Profile diff 是 release gate 的核心訊號</strong>：每次 deploy 後自動對比 baseline、退化幅度過門檻 trigger alert。詳見 <a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a> 跟 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff 卡片</a>。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix 多 DB 統一後 profile 變單純</a> — DB 統一 → application 層 profile 噪音降低 → 退化定位更快。</p>
<h2 id="cardinality-cost-governance">Cardinality cost governance</h2>
<p>效能 observability 的成本經常爆炸、源頭通常是 high cardinality metric。</p>
<p><strong>高 cardinality 來源</strong>：</p>
<ul>
<li>per-user metric（user_id label）</li>
<li>per-request metric（request_id label）</li>
<li>per-trace metric（trace_id label）</li>
</ul>
<p><strong>為什麼會爆</strong>：Prometheus 等 metric system 為每個 label 組合存獨立 time series、cardinality = 所有 label value 的笛卡爾積。100 萬 user × 100 endpoint × 10 region = 10 億 time series、儲存爆炸。</p>
<p><strong>對策</strong>：</p>
<ul>
<li>high cardinality 資訊放 log / trace、不放 metric</li>
<li>metric label 限制在 low-cardinality 維度（service、endpoint、region、status）</li>
<li>真的需要 high-cardinality 分析、用 sampled trace + log query</li>
</ul>
<p>對應 <a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">04.10 cardinality cost governance</a>、跟 <a href="/blog/backend/knowledge-cards/metric-cardinality/" data-link-title="Metric Cardinality" data-link-desc="說明 metric label 組合數量如何影響觀測成本與查詢穩定性">Metric Cardinality 卡片</a>。</p>
<h2 id="訊號跟-slo-對接">訊號跟 SLO 對接</h2>
<p>最後一層整合：每個 saturation metric 都要對應一個 SLO threshold、訊號驅動行動。</p>
<p><strong>訊號 → 行動鏈</strong>：</p>
<ul>
<li>saturation metric 超 threshold → trigger alert</li>
<li>alert 觸發 → trigger autoscaler / runbook / oncall</li>
<li>持續超 threshold → trigger <a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> burn alert</li>
<li>error budget 用完 → trigger release freeze</li>
</ul>
<p><strong>Alert 不要太敏感</strong>：</p>
<ul>
<li>false positive 浪費 oncall、長期會 alert fatigue（<a href="/blog/backend/knowledge-cards/alert-fatigue/" data-link-title="Alert Fatigue" data-link-desc="說明過多低品質告警如何降低 on-call 反應品質">Alert Fatigue 卡片</a>）</li>
<li>用 multi-window multi-burn-rate alert（Google SRE 推薦）</li>
<li>用 symptom-based alert（業務影響）而非 cause-based alert（單一資源）</li>
</ul>
<p>跟 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a> 直接對接。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads 99.999%</a></td>
          <td>SLO 5 個 9 的訊號治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 12 個月 99.999%</a></td>
          <td>滾動 SLO 觀測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi p99 分解</a></td>
          <td>ML inference 多 stage latency budget</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech p95 是業務 KPI</a></td>
          <td>latency 不只是技術指標</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
<li>跨模組：<a href="/blog/backend/04-observability/" data-link-title="模組四：可觀測性平台" data-link-desc="整理 log、metric、trace、dashboard 與 alert 的後端操作實務">04 可觀測性模組</a>（基礎訊號）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/use-method/" data-link-title="USE Method" data-link-desc="Brendan Gregg 提出的資源層 Utilization / Saturation / Errors 三維度量測法">USE Method</a></li>
<li><a href="/blog/backend/knowledge-cards/red-method/" data-link-title="RED Method" data-link-desc="Tom Wilkie 提出的請求層 Rate / Errors / Duration 三維度量測法">RED Method</a></li>
<li><a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">Tail Latency</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
</ul>
]]></content:encoded></item><item><title>9.9 Performance Improvement Loop</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/improvement-loop/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Improvement loop 的責任是把效能優化從「事件型 hotfix」變成「持續改進的工程流程」。沒有 loop 時、效能問題靠 oncall 觸發、改了又改、改完又退化；有 loop 之後、每次 release 都通過 perf gate、退化在發布前就攔住。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate&lt;/a> 的關係：06.13 是 release gate 的一個環節、9.9 是這個 gate 背後的完整工程閉環。06.13 處理「進 gate 後怎麼判斷」、9.9 處理「進 gate 前怎麼產生比較資料」。&lt;/p>
&lt;p>本章聚焦在 &lt;em>閉環設計&lt;/em> — 怎麼建 baseline、怎麼跑 re-test、怎麼用 profile diff、怎麼整合 CI。讀完後讀者能設計一個 perf improvement workflow、不是只有 ad-hoc 壓測。&lt;/p>
&lt;h2 id="loop-五個階段">Loop 五個階段&lt;/h2>
&lt;p>完整的 improvement loop 包含五個階段、缺一不可：&lt;/p>
&lt;p>&lt;strong>1. Baseline 建立&lt;/strong>：壓測 + profile 取得「當前正常」snapshot。
&lt;strong>2. 變更 + re-test&lt;/strong>：每次 release candidate 跑壓測、跟 baseline diff。
&lt;strong>3. Profile diff&lt;/strong>：用 flame graph diff 定位退化原因。
&lt;strong>4. Fix&lt;/strong>：rollback 或修正 code path。
&lt;strong>5. Update baseline&lt;/strong>：通過後更新 baseline、進下個 cycle。&lt;/p>
&lt;p>少了 baseline → re-test 沒有比較對象、看絕對數字會錯判。
少了 profile diff → 退化定位靠猜、修錯方向。
少了 update baseline → 永遠跟 old baseline 比、退化累積看不出來。
少了 fix → 退化通過 gate、production 出事。&lt;/p>
&lt;h2 id="baseline-設計">Baseline 設計&lt;/h2>
&lt;p>Baseline 不是「歷史最佳」、是「最低可接受效能」。&lt;/p>
&lt;p>&lt;strong>設計原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>不只一個 baseline、按 workload model 訂多個（不同 endpoint、不同 user tier 各自 baseline）&lt;/li>
&lt;li>baseline 必須可重複：固定 seed、固定資料集、固定環境、固定壓測參數&lt;/li>
&lt;li>定期 review：硬體 / 軟體升級會讓 baseline 該往好的方向走、不更新就是裝盲&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>儲存策略&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>baseline as artifact：存進 release artifact、隨 release 帶走&lt;/li>
&lt;li>baseline as code：用 Pulumi / Terraform / dedicated config 管理、可 version control&lt;/li>
&lt;li>baseline as service：dedicated service 管 baseline、提供 query API&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Drift 監控&lt;/strong>：baseline 每月對比上月、看趨勢是否往好方向。drift 超門檻 → re-baseline 並 review 原因。&lt;/p>
&lt;h2 id="profile-diff">Profile diff&lt;/h2>
&lt;p>退化定位的關鍵工具是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">profile diff&lt;/a> — 對比兩次 profile 找 hottest 變化。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Improvement loop 的責任是把效能優化從「事件型 hotfix」變成「持續改進的工程流程」。沒有 loop 時、效能問題靠 oncall 觸發、改了又改、改完又退化；有 loop 之後、每次 release 都通過 perf gate、退化在發布前就攔住。</p>
<p>跟 <a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> 的關係：06.13 是 release gate 的一個環節、9.9 是這個 gate 背後的完整工程閉環。06.13 處理「進 gate 後怎麼判斷」、9.9 處理「進 gate 前怎麼產生比較資料」。</p>
<p>本章聚焦在 <em>閉環設計</em> — 怎麼建 baseline、怎麼跑 re-test、怎麼用 profile diff、怎麼整合 CI。讀完後讀者能設計一個 perf improvement workflow、不是只有 ad-hoc 壓測。</p>
<h2 id="loop-五個階段">Loop 五個階段</h2>
<p>完整的 improvement loop 包含五個階段、缺一不可：</p>
<p><strong>1. Baseline 建立</strong>：壓測 + profile 取得「當前正常」snapshot。
<strong>2. 變更 + re-test</strong>：每次 release candidate 跑壓測、跟 baseline diff。
<strong>3. Profile diff</strong>：用 flame graph diff 定位退化原因。
<strong>4. Fix</strong>：rollback 或修正 code path。
<strong>5. Update baseline</strong>：通過後更新 baseline、進下個 cycle。</p>
<p>少了 baseline → re-test 沒有比較對象、看絕對數字會錯判。
少了 profile diff → 退化定位靠猜、修錯方向。
少了 update baseline → 永遠跟 old baseline 比、退化累積看不出來。
少了 fix → 退化通過 gate、production 出事。</p>
<h2 id="baseline-設計">Baseline 設計</h2>
<p>Baseline 不是「歷史最佳」、是「最低可接受效能」。</p>
<p><strong>設計原則</strong>：</p>
<ul>
<li>不只一個 baseline、按 workload model 訂多個（不同 endpoint、不同 user tier 各自 baseline）</li>
<li>baseline 必須可重複：固定 seed、固定資料集、固定環境、固定壓測參數</li>
<li>定期 review：硬體 / 軟體升級會讓 baseline 該往好的方向走、不更新就是裝盲</li>
</ul>
<p><strong>儲存策略</strong>：</p>
<ul>
<li>baseline as artifact：存進 release artifact、隨 release 帶走</li>
<li>baseline as code：用 Pulumi / Terraform / dedicated config 管理、可 version control</li>
<li>baseline as service：dedicated service 管 baseline、提供 query API</li>
</ul>
<p><strong>Drift 監控</strong>：baseline 每月對比上月、看趨勢是否往好方向。drift 超門檻 → re-baseline 並 review 原因。</p>
<h2 id="profile-diff">Profile diff</h2>
<p>退化定位的關鍵工具是 <a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">profile diff</a> — 對比兩次 profile 找 hottest 變化。</p>
<p><strong>工具實作</strong>：</p>
<ul>
<li>Brendan Gregg 的 differential flame graph：開源、需要手動 generate</li>
<li>Pyroscope diff：UI 直接對比兩個時間段</li>
<li>Datadog Continuous Profiler diff：跟 deployment marker 整合</li>
<li>Parca compare：CNCF 標準</li>
<li>AWS CodeGuru Profiler：自動偵測 CPU / memory anti-pattern</li>
</ul>
<p><strong>正確使用方法</strong>：</p>
<ul>
<li>在 <em>相同負載 + 相同硬體 + 相同 sampling rate</em> 下取兩次 profile</li>
<li>比較 <em>相對變化</em>、不是絕對 CPU%</li>
<li>看 wider stack（不只看 leaf function）找 systemic regression</li>
</ul>
<p><strong>Profile diff 結果通常需要工程師判讀</strong>：「多花 20% CPU 但 throughput 多 50%」可能是好變化、不能純自動化判斷退化是否可接受。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 統一</a> — DB 層統一後 profile diff 噪音降低、退化來源更容易識別。</p>
<h2 id="regression-gate-整合-ci">Regression gate 整合 CI</h2>
<p>效能改進閉環必須整合到 CI、不能只在 release 前一次性跑。</p>
<p><strong>Multi-tier 壓測策略</strong>：</p>
<ul>
<li>每個 PR：跑 lightweight perf test（單 endpoint、5 分鐘）、合併前比 baseline</li>
<li>主分支 nightly：跑 medium perf test（多 endpoint、30 分鐘）</li>
<li>Release candidate：跑 complete perf test（完整 workload model、數小時）</li>
</ul>
<p><strong>Gate 觸發條件</strong>：</p>
<ul>
<li>p99 退化 &gt; X%（例如 10%）</li>
<li>吞吐降 &gt; Y%（例如 5%）</li>
<li>error rate 升 &gt; Z%</li>
<li>cost per request 升 &gt; W%</li>
</ul>
<p><strong>Gate 通過 / 不通過的後果</strong>：</p>
<ul>
<li>通過：自動 promote 到下個 stage（staging / canary / production）</li>
<li>不通過：block release、自動 notify owner、附 profile diff link</li>
</ul>
<p><strong>Gate 太敏感的反模式</strong>：</p>
<ul>
<li>每天 false positive、最後沒人看（alert fatigue）</li>
<li>false positive 來源：壓測環境噪音、baseline drift 未更新、業務變化</li>
<li>對策：multi-window detection（變化必須持續 N 個 sample）、配合 manual override（資深工程師判斷異常正常）</li>
</ul>
<p>對應案例：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> 的實作建議。</p>
<h2 id="canary-perf-check">Canary perf check</h2>
<p><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary perf check</a> 是 release 階段的另一道 perf gate。跟 regression gate（pre-release）對應、是 <em>production</em> 階段的監控。</p>
<p><strong>Canary 階段除了看 error rate、也看</strong>：</p>
<ul>
<li>latency p99 / p999（最先看到的 regression 訊號）</li>
<li>throughput（是否處理變慢）</li>
<li>resource utilization（CPU / RAM / connection 變化）</li>
<li>cost per request（是否更貴）</li>
</ul>
<p><strong>Canary 流量 vs control 流量比較</strong>：</p>
<ul>
<li>同樣流量同樣時段、不同版本的差才有意義</li>
<li>不能拿 canary 跟 historical baseline 比（外部變數太多）</li>
<li>abort condition：canary p99 比 control 退化 &gt; X%</li>
</ul>
<p><strong>漸進放大策略</strong>：1% → 5% → 25% → 50% → 100%、每階段觀察足夠時間（至少 15 分鐘看 long-tail）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day FIS 8x chaos</a> — canary 模式跟 chaos test 並行、確保新版本在故障場景也撐得住。</p>
<h2 id="pre-release-改進迴圈頻率">Pre-release 改進迴圈頻率</h2>
<p>不同層級的 review 在不同節奏：</p>
<ul>
<li><strong>每日 PR 級 perf check</strong>：lightweight、單 endpoint、5 分鐘</li>
<li><strong>每週 release candidate 完整壓測</strong>：完整 workload model、數小時</li>
<li><strong>每月 baseline review + drift 評估</strong>：對比歷史趨勢、決定是否 re-baseline</li>
<li><strong>每季容量地圖 review</strong>：跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 連動</li>
</ul>
<p>頻率不夠 → 退化累積看不到；頻率太高 → 工程資源吃緊。按團隊規模跟 release 節奏調整。</p>
<h2 id="退化的常見來源">退化的常見來源</h2>
<p>知道退化怎麼來、才能設計對應的 detection：</p>
<ul>
<li><strong>新功能引入 N+1 query</strong>：ORM lazy loading、loop 內 query。看 DB call count 變化</li>
<li><strong>ORM 沒下 index、cache miss 飆升</strong>：看 slow query 跟 cache hit rate</li>
<li><strong>第三方 library upgrade 帶來 overhead</strong>：新版本可能多了 telemetry / validation。看 profile diff</li>
<li><strong>GC tuning 變動</strong>：JVM / Go GC config 調整造成 pause time 變化。看 p999</li>
<li><strong>container resource limit 變動</strong>：Kubernetes limit 改、限制更嚴造成 throttling。看 CPU throttling event</li>
</ul>
<h2 id="反模式">反模式</h2>
<ul>
<li><strong>只在 release 前一次性壓測</strong>：退化已累積數月、找不出原因</li>
<li><strong>baseline 不更新</strong>：永遠跟舊版本比、低估目前狀態</li>
<li><strong>改了又改、改完忘記更新 baseline</strong>：下次 release 又跟過時 baseline 比、迴圈失效</li>
<li><strong>缺 profile diff、退化原因靠猜</strong>：修錯方向、退化還在</li>
<li><strong>gate 訊號跟業務無關</strong>：技術指標退化但業務 metric 沒事、被當 false positive</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>統一 DB 後 profile 變單純</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>遷移後重新做 baseline</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day FIS 8x</a></td>
          <td>持續改進的混沌 + 壓測迴圈</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/performance-regression-gate/" data-link-title="6.13 Performance Regression Gate" data-link-desc="把效能 baseline 從一次性壓測變成持續對齊的 release gate，涵蓋 baseline 設定、判讀方法、variance 控制與退化定位">06.13 perf regression gate</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/profile-diff/" data-link-title="Profile Diff" data-link-desc="對比兩次 profile（如 release candidate vs baseline）找出 hottest 變化">Profile Diff</a></li>
<li><a href="/blog/backend/knowledge-cards/continuous-profiling/" data-link-title="Continuous Profiling" data-link-desc="在 production 持續取得低 overhead profile 的觀察方法">Continuous Profiling</a></li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a></li>
</ul>
]]></content:encoded></item><item><title>9.10 Production-Side 驗證</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>Production-side 驗證的責任是回答「staging 過了 production 一定過嗎」。多數 staging 環境的硬體 / 流量 / 資料 / 第三方依賴都跟 production 不一樣、staging 通過不代表 production 安全。本章處理「在 production 安全驗證新配置」的工程做法。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary&lt;/a> 的關係：06.20 走「故障注入」的安全邊界（chaos）、9.10 走「正常負載」的 production 驗證（perf）。兩者方法論類似、目標完全不同。chaos test 是「主動破壞看會不會出事」、production perf validation 是「真實流量看新版本能不能跑」。&lt;/p>
&lt;p>本章四個工具（shadow traffic、dark launch、canary、production-like load test）按 &lt;em>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius&lt;/a>&lt;/em> 從小到大排列、每個適合不同驗證場景。&lt;/p>
&lt;h2 id="shadow-traffic">Shadow traffic&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow traffic&lt;/a> 是 blast radius 最小的工具：複製 production traffic 到新版本、但 &lt;em>不把結果返回用戶&lt;/em>。&lt;/p>
&lt;p>&lt;strong>運作機制&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>用戶看到的還是舊版本回應、體驗不變&lt;/li>
&lt;li>新版本只是「並行跑、看會不會崩」&lt;/li>
&lt;li>新版本的結果可以跟舊版本對比、找出邏輯差異&lt;/li>
&lt;li>對下游的寫入要 &lt;em>特別處理&lt;/em>：要麼寫入 sandbox、要麼 dry-run（純驗證 query plan、不真寫）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>工具實作&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>GoReplay：tcpdump-based 開源、適合 HTTP&lt;/li>
&lt;li>Service mesh shadow（Istio、Linkerd mirror）：mesh 層 mirror、零 application invasion&lt;/li>
&lt;li>AWS VPC Traffic Mirroring：底層網路層、加密 traffic 要另處理&lt;/li>
&lt;li>Diffy（已 deprecated 但概念有效）：dual-write 對比結果&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>適合場景&lt;/strong>：架構大改、想驗證 &lt;em>是否能撐 production traffic&lt;/em> 但不能影響用戶。例如「DB 從 PostgreSQL 換 Aurora、想看新 DB 在真實 query pattern 下穩不穩」。&lt;/p>
&lt;p>&lt;strong>注意事項&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>shadow traffic 也消耗 production 下游資源（DB read、API call）— 必須算進容量&lt;/li>
&lt;li>加密 / PII 資料需要處理&lt;/li>
&lt;li>shadow 通常跑 1-7 天看 long-tail、不是 30 分鐘就下結論&lt;/li>
&lt;/ul>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &amp;#43; 傳統伺服器當慢速消費者、承受 100K&amp;#43; 同時選位 &amp;#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測&lt;/a> — pre-event 壓測但走 staging；real shadow 則是 &lt;em>production-traffic-driven&lt;/em> 而非合成。&lt;/p>
&lt;h2 id="dark-launch">Dark launch&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark launch&lt;/a> 介於 shadow 跟 canary 之間：程式碼上線、走 production traffic、但 &lt;em>UI 入口暫不開放&lt;/em>。&lt;/p>
&lt;p>&lt;strong>跟 shadow 的差別&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>Shadow：traffic 複製、新版本 &lt;em>不寫入真實狀態&lt;/em>&lt;/li>
&lt;li>Dark launch：&lt;em>真實寫入 production&lt;/em>、但用戶看不到 UI&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>運作機制&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>Production-side 驗證的責任是回答「staging 過了 production 一定過嗎」。多數 staging 環境的硬體 / 流量 / 資料 / 第三方依賴都跟 production 不一樣、staging 通過不代表 production 安全。本章處理「在 production 安全驗證新配置」的工程做法。</p>
<p>跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 的關係：06.20 走「故障注入」的安全邊界（chaos）、9.10 走「正常負載」的 production 驗證（perf）。兩者方法論類似、目標完全不同。chaos test 是「主動破壞看會不會出事」、production perf validation 是「真實流量看新版本能不能跑」。</p>
<p>本章四個工具（shadow traffic、dark launch、canary、production-like load test）按 <em><a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">blast radius</a></em> 從小到大排列、每個適合不同驗證場景。</p>
<h2 id="shadow-traffic">Shadow traffic</h2>
<p><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow traffic</a> 是 blast radius 最小的工具：複製 production traffic 到新版本、但 <em>不把結果返回用戶</em>。</p>
<p><strong>運作機制</strong>：</p>
<ul>
<li>用戶看到的還是舊版本回應、體驗不變</li>
<li>新版本只是「並行跑、看會不會崩」</li>
<li>新版本的結果可以跟舊版本對比、找出邏輯差異</li>
<li>對下游的寫入要 <em>特別處理</em>：要麼寫入 sandbox、要麼 dry-run（純驗證 query plan、不真寫）</li>
</ul>
<p><strong>工具實作</strong>：</p>
<ul>
<li>GoReplay：tcpdump-based 開源、適合 HTTP</li>
<li>Service mesh shadow（Istio、Linkerd mirror）：mesh 層 mirror、零 application invasion</li>
<li>AWS VPC Traffic Mirroring：底層網路層、加密 traffic 要另處理</li>
<li>Diffy（已 deprecated 但概念有效）：dual-write 對比結果</li>
</ul>
<p><strong>適合場景</strong>：架構大改、想驗證 <em>是否能撐 production traffic</em> 但不能影響用戶。例如「DB 從 PostgreSQL 換 Aurora、想看新 DB 在真實 query pattern 下穩不穩」。</p>
<p><strong>注意事項</strong>：</p>
<ul>
<li>shadow traffic 也消耗 production 下游資源（DB read、API call）— 必須算進容量</li>
<li>加密 / PII 資料需要處理</li>
<li>shadow 通常跑 1-7 天看 long-tail、不是 30 分鐘就下結論</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — pre-event 壓測但走 staging；real shadow 則是 <em>production-traffic-driven</em> 而非合成。</p>
<h2 id="dark-launch">Dark launch</h2>
<p><a href="/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark launch</a> 介於 shadow 跟 canary 之間：程式碼上線、走 production traffic、但 <em>UI 入口暫不開放</em>。</p>
<p><strong>跟 shadow 的差別</strong>：</p>
<ul>
<li>Shadow：traffic 複製、新版本 <em>不寫入真實狀態</em></li>
<li>Dark launch：<em>真實寫入 production</em>、但用戶看不到 UI</li>
</ul>
<p><strong>運作機制</strong>：</p>
<ul>
<li>後端 code 部署到 production</li>
<li>用 feature flag 控制 UI 暴露</li>
<li>從內部 API、cron job、employee-only access 觸發新功能</li>
<li>真正寫入 production DB / cache / queue</li>
<li>用戶看不到 UI 入口、無感</li>
</ul>
<p><strong>Exit criteria</strong>：</p>
<ul>
<li>跑足夠時間（通常 1-2 週）</li>
<li>內部使用沒有 critical issue</li>
<li>metric 在預期範圍</li>
</ul>
<p><strong>適合場景</strong>：新功能後端風險高、想 production-validate 再開放給用戶。
<strong>不適合</strong>：純 UI 改動（沒有後端風險、直接 canary）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">SeatGeek Virtual Waiting Room</a> 從第三方換到自建、必然有 dark launch 階段驗證 token 配發機制、再正式 cutover。</p>
<h2 id="canary">Canary</h2>
<p>Canary 是 production-side 驗證最常用工具：小比例流量導到新版本、跟舊版本對比。</p>
<p><strong>運作機制</strong>：</p>
<ul>
<li>小比例（1% / 5% / 10%）流量導到新版本</li>
<li>大部分流量（99% / 95% / 90%）走舊版本</li>
<li>比較 perf / error / business metric</li>
<li>通過 → 漸進放大；不通過 → 自動 rollback</li>
</ul>
<p><strong>漸進放大策略</strong>：1% → 5% → 25% → 50% → 100%、每階段觀察足夠時間（至少 15 分鐘看 long-tail）。</p>
<p><strong>自動 rollback 條件</strong>：</p>
<ul>
<li>error rate canary 比 control 高 X%（例如 50%）</li>
<li>p99 latency canary 比 control 退化 X%（例如 10%）</li>
<li>business metric（conversion rate）canary 比 control 低 X%</li>
</ul>
<p><strong>Canary perf check 跟一般 canary 的差異</strong>：</p>
<ul>
<li>一般 canary：看 error rate 為主</li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary perf check</a>：看 latency / throughput / cost、退化通常早於 error rate</li>
</ul>
<p><strong>比較的對象是 control（同時跑的舊版本）、不是 baseline</strong>：同樣流量同樣時段才能對比、不能拿 canary 跟昨天 baseline 比（外部變數太多）。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day pre-event 驗證</a> / <a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel canary across 20 州</a> — 按 region 漸進放大、控制 blast radius。</p>
<h2 id="production-like-load-test">Production-like load test</h2>
<p>當需要驗證 <em>peak 場景</em> 但 production 平日流量達不到時、在 production 跑額外的 synthetic load。</p>
<p><strong>為什麼要在 production 跑</strong>：</p>
<ul>
<li>staging 環境的硬體 / 網路 / 第三方依賴跟 production 不同</li>
<li>staging 沒有 production 級資料量、cache hit pattern 不一樣</li>
<li>只有 production 才能驗證真實 peak</li>
</ul>
<p><strong>風險高、必須有安全邊界</strong>：</p>
<ul>
<li>blast radius 限制（用 dedicated test endpoint、限制影響範圍）</li>
<li>abort condition（什麼訊號觸發停止）</li>
<li>rollback path（rollback 流程跟時間）</li>
<li>通訊（相關 oncall 通知、避免誤判 incident）</li>
</ul>
<p><strong>通常用在</strong>：</p>
<ul>
<li>Pre-event 壓測（Black Friday、Super Bowl、IPL 決賽 前一週）</li>
<li>重大架構變更後驗證</li>
<li>容量規劃 review（每年 / 每季）</li>
</ul>
<p><strong>跟 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 同等嚴格的安全要求</strong>：production 壓測本質是 controlled experiment、必須有 game day-level 的計畫跟人員。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day FIS 8x chaos</a> — 把 chaos test 跟 load test 結合、production-like 驗證；<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 10K t2.micro 壓測</a> — pre-event 大規模壓測模擬實際售票場景。</p>
<h2 id="ab-test-與-perf-對齊">A/B test 與 perf 對齊</h2>
<p>Product A/B test（測試新功能對 conversion 的影響）同時也是 perf A/B test。</p>
<p><strong>為什麼要對齊</strong>：</p>
<ul>
<li>新 feature 可能帶來 perf 退化（多 query、多 component、額外 logic）</li>
<li>純看 conversion lift 會誤判：「conversion 上升、所以 OK」可能掩蓋「但 p99 上升 30%」</li>
<li>A/B 同時看 conversion 跟 perf 兩個 metric</li>
</ul>
<p><strong>Guardrails</strong>：</p>
<ul>
<li>業務 metric 改善 + perf 退化 → 工程判斷是否值得（trade-off review）</li>
<li>業務 metric 沒改善 + perf 退化 → 直接 reject</li>
<li>業務 metric 改善 + perf 改善 → 直接 ship</li>
<li>業務 metric 退化 → 不論 perf 怎樣、reject</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> 的 experiment guardrails。</p>
<h2 id="pre-event-readiness-checkgame-day">Pre-event readiness check（game day）</h2>
<p>大事件前跑「全系統 production-like 壓測」、是 production-side 驗證的整合演練。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a> 直接對接 — game day 是 readiness 流程的一個 stage。</p>
<p>Shopify game day、Stripe game day 是業界範本（<a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases</a> 有完整案例）。</p>
<h2 id="安全邊界設計">安全邊界設計</h2>
<p>任何 production-side 驗證都要有清楚的安全邊界、不能臨機應變。</p>
<p><strong>Blast radius</strong>：</p>
<ul>
<li>影響哪些用戶（X% 流量、特定 cohort、特定 region）</li>
<li>影響哪些 service（受 perf 影響的下游）</li>
<li>影響哪些 metric（哪些 business metric 可能變化）</li>
</ul>
<p><strong>Abort condition</strong>：</p>
<ul>
<li>什麼訊號觸發停止（error rate &gt; X%、latency &gt; Y ms、特定 alert 觸發）</li>
<li>由誰觸發（自動 vs oncall 手動）</li>
<li>觸發後多久內必須完成 abort（&lt; 60 秒）</li>
</ul>
<p><strong>Rollback path</strong>：</p>
<ul>
<li>rollback 流程是什麼（feature flag、deployment rollback、traffic shift）</li>
<li>rollback 需要多久（target &lt; 5 分鐘）</li>
<li>rollback 是否需要 data 處理（已寫入的資料怎麼處理）</li>
</ul>
<p><strong>通訊</strong>：</p>
<ul>
<li>啟動驗證前 notify 哪些 channel</li>
<li>期間 oncall 待命</li>
<li>結束後 retro</li>
</ul>
<h2 id="反模式">反模式</h2>
<ul>
<li><strong>Canary 比例太大</strong>（50% 起跳）：出事影響大、blast radius 失控</li>
<li><strong>沒 control group</strong>：不知道 baseline、看絕對數字會誤判</li>
<li><strong>Canary 跑太短時間</strong>（&lt; 15 分鐘）：看不到 long-tail、看不到 user pattern shift</li>
<li><strong>沒 abort condition</strong>：人工監控失誤就出事、不可預測</li>
<li><strong>shadow traffic 寫入真實狀態</strong>：可能造成 double charge、duplicate notification</li>
<li><strong>production load test 沒 notify 相關團隊</strong>：被當成 incident、誤觸 escalation</li>
</ul>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day FIS 8x</a></td>
          <td>pre-event chaos + perf 驗證</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft 10K t2.micro 壓測</a></td>
          <td>pre-event 大規模壓測</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>跨 20 州 canary 控制 blast radius</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/seatgeek-virtual-waiting-room/" data-link-title="9.C16 SeatGeek：DynamoDB &#43; Lambda 打造的虛擬等候室" data-link-desc="SeatGeek 用 DynamoDB 4 張表 &#43; Lambda Bouncer 實作 flash-sale 限流排隊機制、取代第三方 waiting room 服務">9.C16 SeatGeek</a></td>
          <td>從第三方換到自建的 dark launch</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/improvement-loop/" data-link-title="9.9 Performance Improvement Loop" data-link-desc="壓測 → profile → fix → re-test → release gate 的閉環">9.9 Improvement Loop</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="活動、季節性流量、推廣事件的 capacity readiness 流程">9.11 高峰事件準備</a></li>
<li>跨模組：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> / <a href="/blog/backend/06-reliability/chaos-testing/" data-link-title="6.4 chaos testing" data-link-desc="把故障注入從工具操作升級成可驗證流程：先定義穩態，再按依賴類型設計注入、控制 blast radius 與收集證據">06.4 chaos testing</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/shadow-traffic/" data-link-title="Shadow Traffic" data-link-desc="把 production traffic 複製到新版本驗證、但不返回結果給用戶的測試模式">Shadow Traffic</a></li>
<li><a href="/blog/backend/knowledge-cards/dark-launch/" data-link-title="Dark Launch" data-link-desc="新功能上線但暫不開放 UI 入口、走 production traffic 但對用戶不可見的發布模式">Dark Launch</a></li>
<li><a href="/blog/backend/knowledge-cards/canary-perf-check/" data-link-title="Canary Perf Check" data-link-desc="canary release 中針對 latency / throughput 而非 error rate 的退化檢查">Canary Perf Check</a></li>
</ul>
]]></content:encoded></item><item><title>Rate Limit 實作</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/rate-limit-implementation/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/rate-limit-implementation/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate limit&lt;/a> 的實作分成三個層次：單機 middleware（一個 server instance 內的限速）、分散式限速（多個 instance 共用的限速狀態）、配額設計（不同 client 和 endpoint 的差異化配額）。Rate limit 的概念基礎（token bucket / sliding window / 和背壓的區別）見 &lt;a href="https://tarrragon.github.io/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">DevOps 流量管控&lt;/a>，本章聚焦後端的程式碼實作。&lt;/p>
&lt;h2 id="單機-middleware-實作">單機 Middleware 實作&lt;/h2>
&lt;p>Rate limit middleware 在 HTTP handler 之前攔截請求。每個 request 過一次 limiter，通過就進入 handler，超限就回 429。&lt;/p>
&lt;h3 id="go-實作">Go 實作&lt;/h3>
&lt;p>Go 標準生態的 &lt;code>golang.org/x/time/rate&lt;/code> 提供 token bucket 的 &lt;code>rate.Limiter&lt;/code>。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="s">&amp;#34;golang.org/x/time/rate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1">// 全域 limiter：每秒 100 個 request、burst 上限 200&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">globalLimiter&lt;/span> &lt;span class="p">=&lt;/span> &lt;span class="nx">rate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewLimiter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">100&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">200&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">rateLimitMiddleware&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">next&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Handler&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="k">return&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">HandlerFunc&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="kd">func&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">ResponseWriter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Request&lt;/span>&lt;span class="p">)&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="k">if&lt;/span> &lt;span class="p">!&lt;/span>&lt;span class="nx">globalLimiter&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Allow&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">w&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Header&lt;/span>&lt;span class="p">().&lt;/span>&lt;span class="nf">Set&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;Retry-After&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Error&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s">&amp;#34;Too Many Requests&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">http&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">StatusTooManyRequests&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="nx">next&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">ServeHTTP&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">w&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="p">})&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="per-client-限速">Per-client 限速&lt;/h3>
&lt;p>全域 limiter 對所有 client 共用一個配額。Per-client 限速讓每個 client（by API key、IP、或 tenant ID）有各自的配額。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-go" data-lang="go">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">var&lt;/span> &lt;span class="nx">clients&lt;/span> &lt;span class="nx">sync&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Map&lt;/span> &lt;span class="c1">// map[string]*rate.Limiter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kd">func&lt;/span> &lt;span class="nf">getClientLimiter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">clientID&lt;/span> &lt;span class="kt">string&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="o">*&lt;/span>&lt;span class="nx">rate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Limiter&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="k">if&lt;/span> &lt;span class="nx">limiter&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">ok&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">clients&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Load&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">clientID&lt;/span>&lt;span class="p">);&lt;/span> &lt;span class="nx">ok&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="k">return&lt;/span> &lt;span class="nx">limiter&lt;/span>&lt;span class="p">.(&lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="nx">rate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">Limiter&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="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="nx">limiter&lt;/span> &lt;span class="o">:=&lt;/span> &lt;span class="nx">rate&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">NewLimiter&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="mi">20&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="c1">// 每 client 每秒 10 個&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="nx">clients&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nf">Store&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">clientID&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">limiter&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="nx">limiter&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Per-client limiter 用 &lt;code>sync.Map&lt;/code> 存、首次出現的 client 自動建立 limiter。長期運行的服務需要定期清理不再活躍的 client limiter（用 goroutine + ticker 掃描最後使用時間）。&lt;/p>
&lt;h3 id="回應格式">回應格式&lt;/h3>
&lt;p>超限時的 HTTP response 需要帶足夠資訊讓 client 做正確的重試決策。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">HTTP/1.1 429 Too Many Requests
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">Retry-After: 1
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">X-RateLimit-Limit: 100
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">X-RateLimit-Remaining: 0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">X-RateLimit-Reset: 1719014400&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>Retry-After&lt;/code> 告訴 client 等多久再試（秒數或 HTTP date）。&lt;code>X-RateLimit-*&lt;/code> headers 不是 RFC 標準但被廣泛使用（GitHub API、Stripe API 都用），讓 client 在被限速前就知道剩餘配額。&lt;/p>
&lt;h2 id="分散式限速redis-backed">分散式限速（Redis-backed）&lt;/h2>
&lt;p>單機 limiter 的計數存在 process 記憶體中。多個 server instance 各自有獨立的 limiter，client 的請求被 load balancer 分配到不同 instance 時，每個 instance 只看到部分請求 — 全域限速失效。&lt;/p>
&lt;p>Redis 做共用的計數儲存，所有 instance 查同一個 counter。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate limit</a> 的實作分成三個層次：單機 middleware（一個 server instance 內的限速）、分散式限速（多個 instance 共用的限速狀態）、配額設計（不同 client 和 endpoint 的差異化配額）。Rate limit 的概念基礎（token bucket / sliding window / 和背壓的區別）見 <a href="/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">DevOps 流量管控</a>，本章聚焦後端的程式碼實作。</p>
<h2 id="單機-middleware-實作">單機 Middleware 實作</h2>
<p>Rate limit middleware 在 HTTP handler 之前攔截請求。每個 request 過一次 limiter，通過就進入 handler，超限就回 429。</p>
<h3 id="go-實作">Go 實作</h3>
<p>Go 標準生態的 <code>golang.org/x/time/rate</code> 提供 token bucket 的 <code>rate.Limiter</code>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="s">&#34;golang.org/x/time/rate&#34;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">// 全域 limiter：每秒 100 個 request、burst 上限 200</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">var</span> <span class="nx">globalLimiter</span> <span class="p">=</span> <span class="nx">rate</span><span class="p">.</span><span class="nf">NewLimiter</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">200</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">func</span> <span class="nf">rateLimitMiddleware</span><span class="p">(</span><span class="nx">next</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span><span class="p">)</span> <span class="nx">http</span><span class="p">.</span><span class="nx">Handler</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="nx">http</span><span class="p">.</span><span class="nf">HandlerFunc</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">w</span> <span class="nx">http</span><span class="p">.</span><span class="nx">ResponseWriter</span><span class="p">,</span> <span class="nx">r</span> <span class="o">*</span><span class="nx">http</span><span class="p">.</span><span class="nx">Request</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">if</span> <span class="p">!</span><span class="nx">globalLimiter</span><span class="p">.</span><span class="nf">Allow</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="nx">w</span><span class="p">.</span><span class="nf">Header</span><span class="p">().</span><span class="nf">Set</span><span class="p">(</span><span class="s">&#34;Retry-After&#34;</span><span class="p">,</span> <span class="s">&#34;1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="nx">http</span><span class="p">.</span><span class="nf">Error</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="s">&#34;Too Many Requests&#34;</span><span class="p">,</span> <span class="nx">http</span><span class="p">.</span><span class="nx">StatusTooManyRequests</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">            <span class="k">return</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="nx">next</span><span class="p">.</span><span class="nf">ServeHTTP</span><span class="p">(</span><span class="nx">w</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">})</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h3 id="per-client-限速">Per-client 限速</h3>
<p>全域 limiter 對所有 client 共用一個配額。Per-client 限速讓每個 client（by API key、IP、或 tenant ID）有各自的配額。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">clients</span> <span class="nx">sync</span><span class="p">.</span><span class="nx">Map</span> <span class="c1">// map[string]*rate.Limiter</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">func</span> <span class="nf">getClientLimiter</span><span class="p">(</span><span class="nx">clientID</span> <span class="kt">string</span><span class="p">)</span> <span class="o">*</span><span class="nx">rate</span><span class="p">.</span><span class="nx">Limiter</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="nx">limiter</span><span class="p">,</span> <span class="nx">ok</span> <span class="o">:=</span> <span class="nx">clients</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="nx">clientID</span><span class="p">);</span> <span class="nx">ok</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">return</span> <span class="nx">limiter</span><span class="p">.(</span><span class="o">*</span><span class="nx">rate</span><span class="p">.</span><span class="nx">Limiter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">limiter</span> <span class="o">:=</span> <span class="nx">rate</span><span class="p">.</span><span class="nf">NewLimiter</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">20</span><span class="p">)</span> <span class="c1">// 每 client 每秒 10 個</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">clients</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">clientID</span><span class="p">,</span> <span class="nx">limiter</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">return</span> <span class="nx">limiter</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>Per-client limiter 用 <code>sync.Map</code> 存、首次出現的 client 自動建立 limiter。長期運行的服務需要定期清理不再活躍的 client limiter（用 goroutine + ticker 掃描最後使用時間）。</p>
<h3 id="回應格式">回應格式</h3>
<p>超限時的 HTTP response 需要帶足夠資訊讓 client 做正確的重試決策。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">HTTP/1.1 429 Too Many Requests
</span></span><span class="line"><span class="ln">2</span><span class="cl">Retry-After: 1
</span></span><span class="line"><span class="ln">3</span><span class="cl">X-RateLimit-Limit: 100
</span></span><span class="line"><span class="ln">4</span><span class="cl">X-RateLimit-Remaining: 0
</span></span><span class="line"><span class="ln">5</span><span class="cl">X-RateLimit-Reset: 1719014400</span></span></code></pre></div><p><code>Retry-After</code> 告訴 client 等多久再試（秒數或 HTTP date）。<code>X-RateLimit-*</code> headers 不是 RFC 標準但被廣泛使用（GitHub API、Stripe API 都用），讓 client 在被限速前就知道剩餘配額。</p>
<h2 id="分散式限速redis-backed">分散式限速（Redis-backed）</h2>
<p>單機 limiter 的計數存在 process 記憶體中。多個 server instance 各自有獨立的 limiter，client 的請求被 load balancer 分配到不同 instance 時，每個 instance 只看到部分請求 — 全域限速失效。</p>
<p>Redis 做共用的計數儲存，所有 instance 查同一個 counter。</p>
<h3 id="sliding-window-counter">Sliding Window Counter</h3>
<p>用 Redis 的 INCR + EXPIRE 實作 sliding window counter。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">-- Redis Lua script（原子操作）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kd">local</span> <span class="n">key</span> <span class="o">=</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kd">local</span> <span class="n">limit</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kd">local</span> <span class="n">window</span> <span class="o">=</span> <span class="n">tonumber</span><span class="p">(</span><span class="n">ARGV</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kd">local</span> <span class="n">current</span> <span class="o">=</span> <span class="n">redis.call</span><span class="p">(</span><span class="s1">&#39;INCR&#39;</span><span class="p">,</span> <span class="n">key</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kr">if</span> <span class="n">current</span> <span class="o">==</span> <span class="mi">1</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">redis.call</span><span class="p">(</span><span class="s1">&#39;EXPIRE&#39;</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">window</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kr">if</span> <span class="n">current</span> <span class="o">&gt;</span> <span class="n">limit</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="kr">return</span> <span class="mi">0</span>  <span class="c1">-- 超限</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="kr">end</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="kr">return</span> <span class="mi">1</span>      <span class="c1">-- 通過</span></span></span></code></pre></div><p>Key 的設計：<code>ratelimit:{client_id}:{endpoint}:{window_start}</code>。Window start 用當前時間截斷到秒或分鐘（如 <code>1719014400</code>），每個窗口一個 key，EXPIRE 自動清理過期窗口。</p>
<h3 id="現成套件">現成套件</h3>
<p>自己寫 Lua script 適合學習，production 用現成套件更可靠：</p>
<table>
  <thead>
      <tr>
          <th>語言</th>
          <th>套件</th>
          <th>特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Go</td>
          <td><code>go-redis/redis_rate</code></td>
          <td>Token bucket 演算法、原子操作、直接整合 go-redis</td>
      </tr>
      <tr>
          <td>Node</td>
          <td><code>rate-limit-redis</code> + <code>express-rate-limit</code></td>
          <td>Express middleware、Redis store 外掛</td>
      </tr>
      <tr>
          <td>Python</td>
          <td><code>limits</code> + Redis backend</td>
          <td>多演算法支援（fixed window / sliding window / token bucket）</td>
      </tr>
  </tbody>
</table>
<h2 id="配額設計">配額設計</h2>
<h3 id="差異化配額">差異化配額</h3>
<p>不同的 endpoint 和 client 有不同的配額需求。搜尋 API 比列表 API 消耗更多計算資源，應該有更低的速率上限。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>配額範例</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Per-API key</td>
          <td>1000 req/min</td>
          <td>每個 client 的公平上限</td>
      </tr>
      <tr>
          <td>Per-endpoint</td>
          <td>搜尋 100 req/min、列表 500 req/min</td>
          <td>搜尋比列表貴</td>
      </tr>
      <tr>
          <td>Per-tenant</td>
          <td>免費 100 req/min、付費 10000 req/min</td>
          <td>商業差異化</td>
      </tr>
  </tbody>
</table>
<h3 id="配額溢出的處理">配額溢出的處理</h3>
<p>超限時的處理策略依業務需求決定：</p>
<p><strong>Reject（429）</strong>：直接拒絕。最簡單，適合 API 服務。Client 收到 429 後按 Retry-After 重試。</p>
<p><strong>Queue（排隊等）</strong>：超限的請求進入等待隊列，按順序處理。適合不能丟棄的操作（付款確認、訂單建立）。代價是 client 端等待時間增加。</p>
<p><strong>Degrade（降級回應）</strong>：超限時回傳簡化版的回應（cached 結果、摘要而非完整資料）。適合讀取操作。</p>
<h2 id="和-monitoring-的整合">和 Monitoring 的整合</h2>
<p>Rate limit 的命中事件應該記入監控系統，讓團隊知道哪些 client 在撞限速、哪些 endpoint 的配額是否合理。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// Rate limit hit 時送 metric 事件</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nx">monitor</span><span class="p">.</span><span class="nf">Metric</span><span class="p">(</span><span class="s">&#34;ratelimit.hit&#34;</span><span class="p">,</span> <span class="kd">map</span><span class="p">[</span><span class="kt">string</span><span class="p">]</span><span class="kt">any</span><span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;client_id&#34;</span><span class="p">:</span> <span class="nx">clientID</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="s">&#34;endpoint&#34;</span><span class="p">:</span>  <span class="nx">r</span><span class="p">.</span><span class="nx">URL</span><span class="p">.</span><span class="nx">Path</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="s">&#34;limit&#34;</span><span class="p">:</span>     <span class="mi">100</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="s">&#34;window&#34;</span><span class="p">:</span>    <span class="s">&#34;1m&#34;</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>Dashboard 視圖：rate limit hit 的時間趨勢 + 按 client 和 endpoint 分群。Hit 數持續上升代表配額設太低（正常使用被限速）或某個 client 在濫用。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Rate limit 的概念基礎 → <a href="/blog/devops/03-traffic-management/rate-limiting/" data-link-title="Rate Limiting" data-link-desc="主動限制每個來源的請求速率 — per-client vs global、token bucket vs sliding window、優先級豁免">DevOps 流量管控 — Rate Limiting</a></li>
<li>背壓機制（被動的流量控制）→ <a href="/blog/devops/03-traffic-management/backpressure/" data-link-title="背壓機制" data-link-desc="下游處理慢時上游怎麼減速 — 有限 buffer &#43; 回壓訊號的設計、和 rate limit 的區別">DevOps 背壓機制</a></li>
<li>Rate limit 知識卡 → <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">Rate Limit</a></li>
<li>監控系統中的 ingestion 限速 → <a href="/blog/monitoring/04-collector/ingestion-scaling/" data-link-title="Ingestion Scaling" data-link-desc="四層防線應對 ingestion 端的流量擴展 — SDK 取樣、Collector 背壓、水平擴展、Queue 解耦">Monitoring Ingestion Scaling</a></li>
</ul>
]]></content:encoded></item><item><title>9.11 高峰事件準備</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>高峰事件準備的責任是把「事件臨頭才動手」變成「事前數週流程化準備」。沒有 readiness 流程時、年度活動靠 oncall 撐、出事率高；有流程之後、活動成「routine event」、工程資源穩定釋放。&lt;/p>
&lt;p>本章 &lt;em>是&lt;/em> &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 在「事件型場景」的應用組合、不重新建立方法論。要看具體方法回到那兩章、本章聚焦在 &lt;em>流程整合&lt;/em>。&lt;/p>
&lt;p>讀完後讀者能設計一個 T-90 → T-0 的事件準備時程、回答「Black Friday 該怎麼準備、Super Bowl 該怎麼準備、新片發布該怎麼準備」。&lt;/p>
&lt;h2 id="事件分類五種負載形狀">事件分類：五種負載形狀&lt;/h2>
&lt;p>不同事件對應不同準備強度、第一步要分類。&lt;/p>
&lt;p>&lt;strong>可預期極端峰值&lt;/strong>：年度活動、預售、賽事決賽。提前數月已知時間、業務影響大。例：Prime Day、Black Friday、Super Bowl、IPL 決賽。
&lt;strong>事件型不可預期峰值&lt;/strong>：賽事高潮、突發新聞、KOL 推廣。時間或大小不完全可預測。例：賽事進球瞬間、KOL 帶貨、突發新聞引發的流量。
&lt;strong>Flash-sale 瞬間爆量&lt;/strong>：售票開賣、報名活動、限量搶購。t=0 瞬間爆量、5-30 分鐘結束。例：演唱會售票、限量商品搶購、報名截止前最後一小時。
&lt;strong>產品爆紅 surge&lt;/strong>：新 app 紅、病毒擴散。完全不可預期、流量會隨熱度消退。例：Pokemon GO、ChatGPT 爆紅初期、TikTok challenge。
&lt;strong>結構性 surge&lt;/strong>：COVID 類外部衝擊、永久 baseline 上移。不會回到舊水準。例：COVID 期間遠距工作工具、烏俄戰爭期間能源類 app。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C1 / 9.C13 / 9.C21 / 9.C27 / 9.C29&lt;/a>（predictable）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C2 / 9.C4 / 9.C7 / 9.C28&lt;/a>（event）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C15 / 9.C16 / 9.C17&lt;/a>（flash-sale）/ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C8 / 9.C18&lt;/a>（surge）。&lt;/p>
&lt;h2 id="t-90--t-0-準備時程">T-90 → T-0 準備時程&lt;/h2>
&lt;p>可預期極端峰值的完整準備時程：&lt;/p>
&lt;p>&lt;strong>T-90 天&lt;/strong>：流量 forecast + 容量計畫敲定。確認預期峰值倍數、確認 headroom 比例、確認跨 region / AZ 分布。產出 &lt;em>容量計畫文件&lt;/em>。&lt;/p>
&lt;p>&lt;strong>T-30 天&lt;/strong>：基礎設施 quota 申請。雲端 instance limit、connection pool、API rate limit、DynamoDB throughput、Lambda concurrency 都要 &lt;em>提前申請&lt;/em>、不能事件當天才發現 quota 不夠。AWS Infrastructure Event Management（IEM）等服務在這階段啟動。&lt;/p>
&lt;p>&lt;strong>T-14 天&lt;/strong>：第一輪 production-like 壓測。驗證容量計畫是否真的能撐預期峰值、找出第一輪 bottleneck。&lt;/p>
&lt;p>&lt;strong>T-7 天&lt;/strong>：完整 game day 演練。注入故障場景（DB failure、AZ outage、第三方 quota 耗盡）、驗證降級、failover、rollback 流程。修正最後問題、更新 runbook。&lt;/p>
&lt;p>&lt;strong>T-2 天&lt;/strong>：pre-scaling 開始。CDN cache pre-warm、Lambda provisioned concurrency 啟動、autoscaler scheduled 開始、DB capacity 預先 scale up。避免事件當天還在 boot。&lt;/p>
&lt;p>&lt;strong>T-0 day&lt;/strong>：watch room 待命、runbook 開機可執行。所有相關 oncall 跨團隊聯合 channel、dashboard 集中、escalation path 清楚。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>高峰事件準備的責任是把「事件臨頭才動手」變成「事前數週流程化準備」。沒有 readiness 流程時、年度活動靠 oncall 撐、出事率高；有流程之後、活動成「routine event」、工程資源穩定釋放。</p>
<p>本章 <em>是</em> <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a> 跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 在「事件型場景」的應用組合、不重新建立方法論。要看具體方法回到那兩章、本章聚焦在 <em>流程整合</em>。</p>
<p>讀完後讀者能設計一個 T-90 → T-0 的事件準備時程、回答「Black Friday 該怎麼準備、Super Bowl 該怎麼準備、新片發布該怎麼準備」。</p>
<h2 id="事件分類五種負載形狀">事件分類：五種負載形狀</h2>
<p>不同事件對應不同準備強度、第一步要分類。</p>
<p><strong>可預期極端峰值</strong>：年度活動、預售、賽事決賽。提前數月已知時間、業務影響大。例：Prime Day、Black Friday、Super Bowl、IPL 決賽。
<strong>事件型不可預期峰值</strong>：賽事高潮、突發新聞、KOL 推廣。時間或大小不完全可預測。例：賽事進球瞬間、KOL 帶貨、突發新聞引發的流量。
<strong>Flash-sale 瞬間爆量</strong>：售票開賣、報名活動、限量搶購。t=0 瞬間爆量、5-30 分鐘結束。例：演唱會售票、限量商品搶購、報名截止前最後一小時。
<strong>產品爆紅 surge</strong>：新 app 紅、病毒擴散。完全不可預期、流量會隨熱度消退。例：Pokemon GO、ChatGPT 爆紅初期、TikTok challenge。
<strong>結構性 surge</strong>：COVID 類外部衝擊、永久 baseline 上移。不會回到舊水準。例：COVID 期間遠距工作工具、烏俄戰爭期間能源類 app。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C1 / 9.C13 / 9.C21 / 9.C27 / 9.C29</a>（predictable）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C2 / 9.C4 / 9.C7 / 9.C28</a>（event）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C15 / 9.C16 / 9.C17</a>（flash-sale）/ <a href="/blog/backend/09-performance-capacity/cases/" data-link-title="模組九案例正文" data-link-desc="雲端服務商實戰案例庫 — 從 AWS / GCP / Azure 公開案例整理高併發、峰值流量與容量規劃實踐">9.C8 / 9.C18</a>（surge）。</p>
<h2 id="t-90--t-0-準備時程">T-90 → T-0 準備時程</h2>
<p>可預期極端峰值的完整準備時程：</p>
<p><strong>T-90 天</strong>：流量 forecast + 容量計畫敲定。確認預期峰值倍數、確認 headroom 比例、確認跨 region / AZ 分布。產出 <em>容量計畫文件</em>。</p>
<p><strong>T-30 天</strong>：基礎設施 quota 申請。雲端 instance limit、connection pool、API rate limit、DynamoDB throughput、Lambda concurrency 都要 <em>提前申請</em>、不能事件當天才發現 quota 不夠。AWS Infrastructure Event Management（IEM）等服務在這階段啟動。</p>
<p><strong>T-14 天</strong>：第一輪 production-like 壓測。驗證容量計畫是否真的能撐預期峰值、找出第一輪 bottleneck。</p>
<p><strong>T-7 天</strong>：完整 game day 演練。注入故障場景（DB failure、AZ outage、第三方 quota 耗盡）、驗證降級、failover、rollback 流程。修正最後問題、更新 runbook。</p>
<p><strong>T-2 天</strong>：pre-scaling 開始。CDN cache pre-warm、Lambda provisioned concurrency 啟動、autoscaler scheduled 開始、DB capacity 預先 scale up。避免事件當天還在 boot。</p>
<p><strong>T-0 day</strong>：watch room 待命、runbook 開機可執行。所有相關 oncall 跨團隊聯合 channel、dashboard 集中、escalation path 清楚。</p>
<p><strong>T+7 天</strong>：retro。對比預測 vs 實際、紀錄 incident 跟 near-miss、列下個事件要改的事。寫進 <a href="/blog/backend/06-reliability/cases/" data-link-title="可靠性服務案例庫" data-link-desc="按服務組織的 SRE 實踐案例庫，累積架構脈絡與工程文化">06 cases</a> 或本模組 cases。</p>
<h2 id="pre-scaling-策略">Pre-scaling 策略</h2>
<p>T-2 階段的 pre-scaling 是「不依賴 autoscaler 反應」的容量保險。</p>
<p><strong>Pre-scaling 涵蓋層次</strong>：</p>
<ul>
<li><strong>ELB warm-up</strong>：請 AWS 預先 warm up ELB，避免流量上來時 ELB 自身需要時間擴容</li>
<li><strong>Lambda provisioned concurrency</strong>：預先 boot 一定數量 instance、避免 cold start</li>
<li><strong>DynamoDB / Cosmos DB capacity</strong>：scheduled 提前 scale up</li>
<li><strong>EC2 ASG</strong>：min instances 提前拉高</li>
<li><strong>CDN cache pre-warm</strong>：重要 URL 提前 invalidate / pre-populate</li>
<li><strong>DB connection pool</strong>：應用層提前 warm up connection</li>
<li><strong>Cache warmup</strong>：把 hot key 提前 populate 進 cache</li>
</ul>
<p><strong>Pre-warm window 通常 30 分鐘到 2 小時</strong>、取決於：</p>
<ul>
<li>Instance boot time（VM-based 慢、container 快）</li>
<li>Cache warmup 時間（cold cache 命中率低、要時間 populate）</li>
<li>Connection pool 預熱（DB connection establish 有 latency）</li>
</ul>
<h3 id="cdn-pre-warm-操作細節">CDN Pre-warm 操作細節</h3>
<p>CDN pre-warm 在 T-2 階段是 high-impact 操作、但跟其他 pre-scaling 的特性不同。具體做法：</p>
<ul>
<li><strong>找出活動會大量被讀取的 URL 清單</strong>：商品頁、活動 landing page、新 release 內容</li>
<li><strong>在每個 CDN edge POP 觸發 cache populate</strong>：可以用 vendor warmup API（Cloudflare Argo、Fastly Image Optimizer pre-fetch、Akamai NetStorage push），或從多個 region 發 synthetic request 強制 edge 拉取</li>
<li><strong>驗證 hit ratio 已升高</strong>：用 vendor dashboard 觀察 cache_status=HIT 比例、確認 pre-warm 生效</li>
<li><strong>預估 origin 流量曲線</strong>：pre-warm 完成後、活動開始時 edge miss 流量應該大幅降低、origin 容量規劃可以對應放鬆</li>
</ul>
<p>跟其他 pre-scaling 不同的是 <strong>CDN pre-warm 沒有「容量上限」這個概念</strong> — edge cache 是被動填的、warm 完就是 warm、不像 EC2 / Lambda 那樣需要 reserve 容量。風險不在「填不夠」、在「填錯」（key 不對、TTL 設錯讓 pre-warm 立刻過期）。詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發</a> 的 purge 與 cacheable 判讀。</p>
<p><strong>事件結束後也要 <em>scheduled scale down</em></strong>：autoscaler 通常 scale up 快、scale down 慢、長期 over-provision 浪費錢。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">Tixcraft 30 分鐘擴 130 倍</a> — pre-scaling + Auto Scaling Group + AMI prebuild + ELB warmup 組合；<a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">Prime Day pre-scaling</a> — predictive scaling + scheduled scaling 兩種組合。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling 卡片</a> 跟 <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling 卡片</a>。</p>
<h2 id="watch-room-設計">Watch room 設計</h2>
<p>T-0 當天的指揮中心、跨團隊聯合 channel。</p>
<p><strong>人員配置</strong>：</p>
<ul>
<li>跨團隊聯合 channel：app / infra / network / SRE / business / customer support</li>
<li>24/7 輪班（國際事件可能跨 24 小時）</li>
<li>明確 incident commander（<a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">08.7 incident command roles</a>）</li>
</ul>
<p><strong>Dashboard 集中</strong>：</p>
<ul>
<li>流量 dashboard：總 RPS、按 region 拆分、按 endpoint 拆分</li>
<li>延遲 dashboard：p50 / p95 / p99 即時、按 service 拆分</li>
<li>錯誤 dashboard：error rate、按 endpoint、按 status code</li>
<li>成本 dashboard：當前 hourly cost、預估全天 cost</li>
<li>業務 dashboard：訂單數、轉換率、收入</li>
</ul>
<p><strong>Runbook 隨手可用</strong>：常見問題 → 對應動作的明確指引。不要事件當下還在 wiki 找資料。</p>
<p><strong>Escalation path</strong>：什麼狀況找誰、多久升級。寫成決策樹、不要靠人記。對應 <a href="/blog/backend/08-incident-response/incident-command-roles/" data-link-title="8.2 事故指揮與角色分工" data-link-desc="定義 incident commander 與跨角色協作責任">08.7 incident command roles</a>。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day 卡片</a>。</p>
<h2 id="vendor-緊急支援">Vendor 緊急支援</h2>
<p>戰略事件可以申請 vendor 工程師待命、是「人力 backup」。</p>
<p><strong>AWS Infrastructure Event Management（IEM）</strong>：年度重大事件可以申請、提供 pre-scaling 與專屬監控通道。
<strong>GCP Customer Reliability Engineering（CRE）</strong>：戰略客戶的 24/7 工程支援、能即時為客戶補容量。
<strong>Azure Premier Support + CSAM</strong>：對等服務。</p>
<p><strong>注意</strong>：這類服務通常綁定 enterprise 等級合約、不是所有客戶都能用。設計事件準備時要假設「沒有 vendor 救援」、vendor 是 bonus 而非 primary plan。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">GR8 Tech World Cup IEM</a> — AWS Infrastructure Event Management 在 2022 FIFA World Cup 期間支援；<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO CRE</a> — GCP CRE 即時補容量、撐過 50x surge。</p>
<h2 id="game-day-演練">Game day 演練</h2>
<p>T-7 階段的核心活動、把 readiness 從計畫變實戰。</p>
<p><strong>演練場景</strong>：</p>
<ul>
<li>模擬「事件當天 worst case」</li>
<li>注入故障：DB primary failure、AZ outage、第三方 quota 達標、network partition</li>
<li>演練降級：哪些功能關閉、用戶看到什麼</li>
<li>演練 failover：流量切到備援</li>
<li>演練 rollback：發現新版本問題、能不能快速回退</li>
</ul>
<p><strong>Game day 學習目標</strong>：</p>
<ul>
<li>runbook 不夠詳細 → 補</li>
<li>訊號不夠 → 加 metric / alert</li>
<li>人員不夠 → 排班補</li>
<li>工具不夠 → 工程補</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/cases/shopify/" data-link-title="Shopify" data-link-desc="Shopify BFCM Scaling / Pod-based Isolation / Capacity Planning">06 cases Shopify game day</a> — Shopify game day 是業界範本、值得直接參考。</p>
<h2 id="event-tier-分級">Event tier 分級</h2>
<p>不同事件規模對應不同準備強度、不能一律照 T-90 流程跑。</p>
<p><strong>Regular event</strong>（每週 promo、small feature launch）：</p>
<ul>
<li>scheduled scaling 即可</li>
<li>無 dedicated watch room</li>
<li>對應 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a> 的常規 release</li>
</ul>
<p><strong>Major event</strong>（季度行銷、新功能發布）：</p>
<ul>
<li>pre-scaling + watch room</li>
<li>簡化版 T-14 → T-0 流程</li>
<li>跨 team coordination</li>
</ul>
<p><strong>Critical event</strong>（年度大促、Super Bowl、IPL）：</p>
<ul>
<li>完整 T-90 流程</li>
<li>vendor IEM + game day</li>
<li>24/7 watch room</li>
<li>C-level visibility</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel</a> regular game → playoff → Super Bowl 三 tier — NFL 賽季 baseline → playoffs 升 2-3x → championship 升 4-5x → Super Bowl 升 5-10x、每 tier 對應不同準備強度。</p>
<h2 id="事後-retro">事後 retro</h2>
<p>T+7 retro 是讓 readiness 持續改進的關鍵。</p>
<p><strong>Retro 必答的問題</strong>：</p>
<ul>
<li>流量 forecast 跟實際差多少？（forecast 改進方向）</li>
<li>容量 utilization 峰值多少？（headroom 是否合適）</li>
<li>有沒有 incident 跟 near-miss？（runbook 更新方向）</li>
<li>下個事件要改的事是什麼？</li>
</ul>
<p><strong>Retro 產出</strong>：</p>
<ul>
<li>forecast 改進建議（給 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6</a>）</li>
<li>新 runbook 或 runbook 更新</li>
<li>新 monitoring / alert</li>
<li>新工程任務（補容量、補工具）</li>
</ul>
<p>對應 <a href="/blog/backend/08-incident-response/post-incident-review/" data-link-title="8.5 復盤與改進追蹤" data-link-desc="把 RCA 與 action items 轉成可驗證閉環">08.13 post-incident review</a> — retro 不只用在 incident、event readiness 也需要。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/aws-prime-day-extreme-scale-2025/" data-link-title="9.C1 AWS Prime Day 2025：可預期極端峰值的 dogfood" data-link-desc="Amazon 自家服務在 Prime Day 2025 的峰值數字 — 一年一次可預期峰值的容量設計參考">9.C1 Prime Day</a></td>
          <td>可預期極端峰值教科書範本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>flash-sale T-2 pre-scaling</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/hotstar-ipl-eighteen-million-concurrent/" data-link-title="9.C13 Disney&#43; Hotstar：IPL 板球決賽 1860 萬人同時直播" data-link-desc="Hotstar 在 IPL 板球決賽創下 1860 萬同時觀看的全球直播紀錄、CDN 與全球邊緣容量極限">9.C13 Hotstar IPL</a></td>
          <td>全球直播 watch room</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/gr8-tech-ai-predicted-betting-peak/" data-link-title="9.C2 GR8 Tech：AI 預測式自動擴容下的體育博彩高峰" data-link-desc="AI 預測 &#43; EKS 自動擴容怎麼在 25ms p95 下承載 54000 TPS 體育博彩峰值流量">9.C2 GR8 Tech</a></td>
          <td>AWS IEM + 自家 AI 預測組合</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a></td>
          <td>event tier 分級（playoff → SB）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">9.C8 Pokemon GO</a></td>
          <td>surge 場景的 vendor 救援（CRE）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> / <a href="/blog/backend/09-performance-capacity/production-validation/" data-link-title="9.10 Production-Side 驗證" data-link-desc="shadow traffic、dark launch、canary、production-like load test">9.10 Production-Side 驗證</a></li>
<li>上游：<a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>（pre-scaling 前要分辨可不可水平擴展）</li>
<li>跨模組：<a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a>（CDN pre-warm / origin protection 是 T-2 核心）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">06.20 experiment safety boundary</a> / <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 事故處理模組</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">Predictive Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a></li>
<li><a href="/blog/backend/knowledge-cards/game-day/" data-link-title="Game Day" data-link-desc="說明事故演練如何驗證流程、工具與團隊協作">Game Day</a></li>
<li><a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.12 SLO 與 Performance Budget</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>SLO 與 performance budget 的責任是讓容量決策有「可衡量的目標 + 可審查的代價」。沒有 SLO 時、容量規劃容易變「越大越好」、沒邊界；有 SLO + budget 之後、所有決策都能回答「是否在 budget 內」、「超出 budget 該怎麼辦」。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget&lt;/a> 的關係：06.6 處理「可靠性 SLO」（用 error budget 凍結 release）、9.12 處理「效能 SLO」（用 performance budget 約束容量）。兩者用同一套方法論、目標不同。讀者可以把本章當作 06.6 的 &lt;em>效能對應&lt;/em> 章節。&lt;/p>
&lt;p>本章覆蓋 SLI/SLO/SLA 分層、latency budget 分解、performance budget vs error budget、SLO 等級的成本含義、多 SLO 對齊、SLO drift 維護。讀完後讀者能設計一套完整的 SLO + budget 系統、把容量決策跟 SLO 對接。&lt;/p>
&lt;h2 id="sli--slo--sla-三層分清">SLI / SLO / SLA 三層分清&lt;/h2>
&lt;p>三個名詞常被混用、實際是三個不同層的概念。&lt;/p>
&lt;p>&lt;strong>SLI（Service Level Indicator）&lt;/strong>：客觀量測值。p99 latency、availability、throughput、error rate 都是 SLI。
&lt;strong>SLO（Service Level Objective）&lt;/strong>：團隊內部目標。「99.95% 用戶請求 &amp;lt; 500ms」這類具體承諾。
&lt;strong>SLA（Service Level Agreement）&lt;/strong>：對外合約承諾。達不到要退款、違約金、信用補償。&lt;/p>
&lt;p>&lt;strong>SLO 比 SLA 嚴 — 給內部 buffer&lt;/strong>。SLA 訂 99.9%、SLO 訂 99.95% — 萬一 SLO 沒達到、SLA 還沒違約、有反應時間。&lt;/p>
&lt;p>&lt;strong>容量規劃針對 SLO、不是 SLA&lt;/strong>：SLA 是「最低不能跌破」、SLO 才是「日常目標」。用 SLA 做容量規劃會經常 violate SLA、給用戶 / 客戶不好體驗。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO 卡片&lt;/a>。&lt;/p>
&lt;h2 id="latency-budget-分解">Latency budget 分解&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency budget&lt;/a> 是把 SLO 翻成可分解工程目標的關鍵工具。&lt;/p>
&lt;p>&lt;strong>從 end-to-end latency 開始&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>用戶感受到的 latency：DNS resolution + TLS handshake + CDN + load balancer + application + cache + DB + serialization + network back&lt;/li>
&lt;li>SLO 訂在 user-perceived：例如「p99 end-to-end &amp;lt; 500ms」&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>拆到每個 stage 的 budget&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>DNS：5ms（assume cached）&lt;/li>
&lt;li>TLS handshake：50ms（first request）&lt;/li>
&lt;li>CDN：20ms&lt;/li>
&lt;li>Load balancer：5ms&lt;/li>
&lt;li>Application：100ms&lt;/li>
&lt;li>Cache lookup：5ms（hit）/ 100ms（miss）&lt;/li>
&lt;li>DB query：30ms&lt;/li>
&lt;li>Serialization：10ms&lt;/li>
&lt;li>Network return：15ms&lt;/li>
&lt;li>&lt;strong>總和&lt;/strong>：240ms（cache hit）/ 335ms（miss）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>每個 stage 的 budget 必須 &lt;em>跟 SLO 對齊&lt;/em>&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>SLO 與 performance budget 的責任是讓容量決策有「可衡量的目標 + 可審查的代價」。沒有 SLO 時、容量規劃容易變「越大越好」、沒邊界；有 SLO + budget 之後、所有決策都能回答「是否在 budget 內」、「超出 budget 該怎麼辦」。</p>
<p>跟 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget</a> 的關係：06.6 處理「可靠性 SLO」（用 error budget 凍結 release）、9.12 處理「效能 SLO」（用 performance budget 約束容量）。兩者用同一套方法論、目標不同。讀者可以把本章當作 06.6 的 <em>效能對應</em> 章節。</p>
<p>本章覆蓋 SLI/SLO/SLA 分層、latency budget 分解、performance budget vs error budget、SLO 等級的成本含義、多 SLO 對齊、SLO drift 維護。讀完後讀者能設計一套完整的 SLO + budget 系統、把容量決策跟 SLO 對接。</p>
<h2 id="sli--slo--sla-三層分清">SLI / SLO / SLA 三層分清</h2>
<p>三個名詞常被混用、實際是三個不同層的概念。</p>
<p><strong>SLI（Service Level Indicator）</strong>：客觀量測值。p99 latency、availability、throughput、error rate 都是 SLI。
<strong>SLO（Service Level Objective）</strong>：團隊內部目標。「99.95% 用戶請求 &lt; 500ms」這類具體承諾。
<strong>SLA（Service Level Agreement）</strong>：對外合約承諾。達不到要退款、違約金、信用補償。</p>
<p><strong>SLO 比 SLA 嚴 — 給內部 buffer</strong>。SLA 訂 99.9%、SLO 訂 99.95% — 萬一 SLO 沒達到、SLA 還沒違約、有反應時間。</p>
<p><strong>容量規劃針對 SLO、不是 SLA</strong>：SLA 是「最低不能跌破」、SLO 才是「日常目標」。用 SLA 做容量規劃會經常 violate SLA、給用戶 / 客戶不好體驗。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO 卡片</a>。</p>
<h2 id="latency-budget-分解">Latency budget 分解</h2>
<p><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency budget</a> 是把 SLO 翻成可分解工程目標的關鍵工具。</p>
<p><strong>從 end-to-end latency 開始</strong>：</p>
<ul>
<li>用戶感受到的 latency：DNS resolution + TLS handshake + CDN + load balancer + application + cache + DB + serialization + network back</li>
<li>SLO 訂在 user-perceived：例如「p99 end-to-end &lt; 500ms」</li>
</ul>
<p><strong>拆到每個 stage 的 budget</strong>：</p>
<ul>
<li>DNS：5ms（assume cached）</li>
<li>TLS handshake：50ms（first request）</li>
<li>CDN：20ms</li>
<li>Load balancer：5ms</li>
<li>Application：100ms</li>
<li>Cache lookup：5ms（hit）/ 100ms（miss）</li>
<li>DB query：30ms</li>
<li>Serialization：10ms</li>
<li>Network return：15ms</li>
<li><strong>總和</strong>：240ms（cache hit）/ 335ms（miss）</li>
</ul>
<p><strong>每個 stage 的 budget 必須 <em>跟 SLO 對齊</em></strong>：</p>
<ul>
<li>每個 stage 加總 = SLO 上限</li>
<li>任何 stage 超 budget → 該 stage 必須改善（不是其他 stage 來補）</li>
<li>每個 stage 必須有 <em>current measurement</em> — 不能訂了沒量</li>
</ul>
<p><strong>Cross-region call 自帶不可壓縮 latency</strong>：</p>
<ul>
<li>同 AZ：&lt; 1ms</li>
<li>跨 AZ：1-2ms</li>
<li>跨 region 同 continent：20-30ms</li>
<li>跨 continent：100-200ms</li>
<li>SLO 訂 50ms 但服務要跨 region 設計 → 不可能達成</li>
</ul>
<p><strong>任何新增 stage 都會吃 budget</strong>：middleware、sidecar、interceptor、API gateway 都會增加 latency。設計時要明確認知這層代價。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase sub-ms</a> — sub-millisecond 反推所有架構選擇（Cluster Placement Group 壓網路、z1d 壓 CPU、RAFT 壓共識）；<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi p99 &lt; 10ms</a> — ML inference 多 stage 各自分配 budget。</p>
<h2 id="performance-budget">Performance budget</h2>
<p><a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">Performance budget</a> 跟 error budget 是 <em>姊妹概念</em> — 用同一套方法論處理可靠性 vs 效能。</p>
<p><strong>Error budget</strong>（<a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6</a>）：</p>
<ul>
<li>每月有允許的 unavailability 額度</li>
<li>例如 SLO 99.95% → error budget = 0.05% × 30 days = 21.6 分鐘 / 月</li>
<li>額度用完 → freeze new release、focus on reliability</li>
</ul>
<p><strong>Performance budget</strong>（本章）：</p>
<ul>
<li>每月有允許的 latency 退化額度</li>
<li>例如「p99 允許比 baseline 高 10ms 連續 X 分鐘」、用 <a href="/blog/backend/knowledge-cards/burn-rate/" data-link-title="Burn Rate" data-link-desc="說明 error budget 消耗速度如何支援告警與事故分級">burn rate</a> alert</li>
<li>額度用完 → freeze new feature release、focus on perf</li>
</ul>
<p><strong>兩個 budget 並列、不衝突</strong>：</p>
<ul>
<li>一個燒一個健康 → 部分 freeze（freeze 對應的那條）</li>
<li>兩個都健康 → 全速 release</li>
<li>兩個都燒 → 全面 freeze、deep review</li>
</ul>
<p><strong>Burn rate alert 比 threshold alert 好</strong>：</p>
<ul>
<li>threshold：p99 &gt; 500ms 就 alert → false positive 多</li>
<li>burn rate：過去 1 小時 budget burn rate &gt; 14.4x 就 alert（Google SRE 推薦）→ 對應「再這樣下去 budget 5 分鐘內燒光」</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">Coinbase 延遲就是收入</a> — 沒 performance budget 等於沒 release control；<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel 多 SLO</a> — 直播 vs 投注不同 budget。</p>
<h2 id="slo-等級的成本含義">SLO 等級的成本含義</h2>
<p>不同 SLO 等級對應不同容量成本、選 SLO 就是選成本。</p>
<table>
  <thead>
      <tr>
          <th>SLO</th>
          <th>年 downtime 上限</th>
          <th>工程含義</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>99%</td>
          <td>年 87.6 小時</td>
          <td>單 AZ 部署可接受</td>
          <td>B2C 內部工具、非 critical SaaS</td>
      </tr>
      <tr>
          <td>99.9%</td>
          <td>年 8.76 小時</td>
          <td>多 AZ、reactive failover</td>
          <td>B2C consumer-facing</td>
      </tr>
      <tr>
          <td>99.95%</td>
          <td>年 4.38 小時</td>
          <td>多 AZ active-active、autoscale 必要</td>
          <td>B2B SaaS minimum</td>
      </tr>
      <tr>
          <td>99.99%</td>
          <td>年 52.6 分鐘</td>
          <td>多 region active-active、無人工介入</td>
          <td>mission-critical SaaS</td>
      </tr>
      <tr>
          <td>99.999%</td>
          <td>年 5.26 分鐘</td>
          <td>全球多 region、即時 failover、人工極少</td>
          <td>金融 / 醫療 / 電信</td>
      </tr>
  </tbody>
</table>
<p><strong>每多一個 9、容量成本指數成長</strong>：</p>
<ul>
<li>99 → 99.9：成本 +30-50%</li>
<li>99.9 → 99.99：成本 +50-100%</li>
<li>99.99 → 99.999：成本 +200-500%</li>
</ul>
<p><strong>選 SLO 不是 marketing 決策、是工程經濟決策</strong>：選太高、燒錢；選太低、用戶不滿。要算 <em>每個 9 對應的業務價值</em>、是否值得對應的容量投資。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">Amazon Ads 99.999%</a> — 廣告計費 1 分鐘斷線損失幾百萬美金、5 個 9 是真實營收邊界；<a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 99.999%</a> — B2B 客服 SaaS、客戶停線 = 客戶失去用戶信任、5 個 9 是合約義務。</p>
<h2 id="多-slo-對齊">多 SLO 對齊</h2>
<p>同一系統不同工作負載可以有不同 SLO、按業務重要性分級。</p>
<p><strong>設計原則</strong>：</p>
<ul>
<li>按「業務重要性 × 用戶感知」分級</li>
<li>同一個 endpoint 不同情境可能有不同 SLO（例如登入 vs 結帳）</li>
<li>多 SLO 必須有 <em>優先順序</em>、衝突時知道犧牲哪個</li>
</ul>
<p><strong>範例</strong>：</p>
<table>
  <thead>
      <tr>
          <th>Endpoint</th>
          <th>SLO</th>
          <th>業務影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>登入</td>
          <td>p99 200ms</td>
          <td>用戶 onboarding</td>
      </tr>
      <tr>
          <td>瀏覽商品</td>
          <td>p99 500ms</td>
          <td>用戶 retention</td>
      </tr>
      <tr>
          <td>結帳</td>
          <td>p99 300ms</td>
          <td>直接影響收入</td>
      </tr>
      <tr>
          <td>推薦</td>
          <td>p99 1000ms</td>
          <td>影響 conversion 但非阻斷</td>
      </tr>
  </tbody>
</table>
<p><strong>衝突處理</strong>：當 capacity 不夠時、優先保 <em>結帳</em> 而非 <em>推薦</em>、即使技術上推薦比較好擴容。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">FanDuel</a> 直播秒級 SLO vs 投注毫秒級 SLO、同一個 user 同一場 NFL Super Bowl、兩個服務必須分開部署、各自 SLO。</p>
<h2 id="slo-演進baseline-drift">SLO 演進：baseline drift</h2>
<p><a href="/blog/backend/knowledge-cards/slo-baseline-drift/" data-link-title="SLO Baseline Drift" data-link-desc="SLO baseline 因業務變化 / surge / 架構改動而需要重新校準的現象">SLO 不是訂了就不動</a> — 業務變化要重新校準。</p>
<p><strong>SLO drift 來源</strong>：</p>
<ul>
<li>Structural surge：COVID 類外部衝擊讓 baseline 永久上移</li>
<li>Product change：新 feature 改變用戶 journey</li>
<li>Architectural improvement：DB 換型、cache 加強、CDN 擴點</li>
<li>User behavior：mobile share 上升、跨 region 比例變化</li>
</ul>
<p><strong>Drift 不是 anomaly、是 <em>新常態</em></strong>。</p>
<p><strong>Review 節奏</strong>：</p>
<ul>
<li>每季 review SLO：拉過去 90 天 SLI 分布、看是否需要調整</li>
<li>重大產品改動立即 review</li>
<li>Drift 確認後要更新：alert threshold、autoscaler trigger、performance budget 額度、容量規劃 baseline</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">Zoom 30x COVID</a> — 30 倍成長後 baseline 永久上移、SLO threshold 跟著重新校準、不能套用 COVID 前的標準。</p>
<h2 id="slo-跟容量規劃對接">SLO 跟容量規劃對接</h2>
<p>回到本章開頭的論點 — SLO 是容量決策的目標。</p>
<p><strong>容量公式</strong>：能撐多少 RPS @ SLO 條件。
<strong>規劃時用「SLO-constrained capacity」、不是「max capacity」</strong>：</p>
<ul>
<li>max capacity：絕對極限、進 cliff</li>
<li>SLO-constrained capacity：知道在 SLO 條件下能撐多少</li>
<li>兩者差 30-50%（headroom）</li>
</ul>
<p><strong>9.4 saturation 找 knee 是技術指標、9.6 容量規劃用 SLO-constrained knee</strong>：</p>
<ul>
<li>saturation 在 utilization 80% 時開始</li>
<li>但 SLO 可能要求 utilization 60% 以下</li>
<li>容量規劃用 60% 而非 80%</li>
</ul>
<p><strong>跟 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本工程</a> 對接</strong>：</p>
<ul>
<li>每多一個 9 多花多少錢</li>
<li>業務需要這個 9 嗎</li>
<li>不需要的話降 SLO 省成本</li>
</ul>
<h2 id="slo-跟-performance-budget-一起用">SLO 跟 performance budget 一起用</h2>
<p>最後的整合 — error budget + performance budget 一起治理 release 節奏。</p>
<p><strong>Error budget 控制 <em>變更節奏</em></strong>：</p>
<ul>
<li>error budget 健康 → release 可以快</li>
<li>error budget 燒光 → freeze release</li>
</ul>
<p><strong>Performance budget 控制 <em>容量決策</em></strong>：</p>
<ul>
<li>performance budget 健康 → 新 feature 可以引入 perf cost</li>
<li>performance budget 燒光 → freeze new feature</li>
</ul>
<p><strong>兩個 budget 並列</strong>：</p>
<ul>
<li>都健康 → 全速 release + 新 feature</li>
<li>error 健康 + perf 燒 → release 但只接 perf-neutral 變更</li>
<li>error 燒 + perf 健康 → 暫停 release、修可靠性</li>
<li>都燒 → 全面 freeze、deep review</li>
</ul>
<p>對應 <a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO</a> 跟 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">06.8 release gate</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a></td>
          <td>latency budget 反推架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 / C24 99.999%</a></td>
          <td>5 個 9 的容量代價</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML stage budget</a></td>
          <td>p99 多 stage 分配</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel 多 SLO</a></td>
          <td>直播 vs 投注不同 SLO 並存</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom</a></td>
          <td>SLO baseline 重新校準</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論</a>（latency budget 反推）</li>
<li>上游：<a href="/blog/backend/09-performance-capacity/saturation-discovery/" data-link-title="9.4 Saturation Discovery" data-link-desc="找出 throughput plateau 與 latency knee 的方法">9.4 Saturation Discovery</a>（SLO-constrained capacity）</li>
<li>跨模組：<a href="/blog/backend/06-reliability/slo-error-budget/" data-link-title="6.6 SLO 與 Error Budget 政策" data-link-desc="把可靠性目標轉成可驗證量測與凍結條件">06.6 SLO 與 Error Budget 政策</a>（可靠性 SLO）</li>
<li>跨模組：<a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">04.16 SLI / SLO 訊號</a>（量測層）</li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/sli-slo/" data-link-title="SLI / SLO" data-link-desc="說明服務品質指標與服務品質目標如何連接產品承諾">SLI / SLO</a></li>
<li><a href="/blog/backend/knowledge-cards/latency-budget/" data-link-title="Latency Budget" data-link-desc="把 user-perceived latency 拆到每個 stage 的配額、反推架構選擇">Latency Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/performance-budget/" data-link-title="Performance Budget" data-link-desc="跟 error budget 同類概念、但用於 latency / throughput 退化的可控額度">Performance Budget</a></li>
<li><a href="/blog/backend/knowledge-cards/slo-baseline-drift/" data-link-title="SLO Baseline Drift" data-link-desc="SLO baseline 因業務變化 / surge / 架構改動而需要重新校準的現象">SLO Baseline Drift</a></li>
<li><a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">Error Budget</a></li>
</ul>
]]></content:encoded></item><item><title>9.13 擴展軸與 Stateless 前提</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/</guid><description>&lt;p>「要換更大的機器、還是要加更多臺機器？」這個問題在規模成長過程中會反覆出現。垂直擴展（scale-up）與水平擴展（scale-out）對應不同壓力來源、各自承擔不同代價：垂直擴展用「換更大的機器」換取簡單、水平擴展用「加更多機器」換取彈性。規劃容量時先判讀自己的壓力屬於哪一種、再選對應的擴展軸 — 選錯軸的代價會在事故時放大。&lt;/p>
&lt;h2 id="兩個軸的責任差異">兩個軸的責任差異&lt;/h2>
&lt;p>垂直擴展指把單一機器換成更高規格（更多 CPU / 記憶體 / IOPS），水平擴展指增加機器數量。同樣是「加資源」，兩者面對的工程問題完全不同。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>垂直擴展（scale-up）&lt;/th>
 &lt;th>水平擴展（scale-out）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>操作單位&lt;/td>
 &lt;td>換一臺機器&lt;/td>
 &lt;td>加 N 臺機器&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>程式假設&lt;/td>
 &lt;td>不需要改&lt;/td>
 &lt;td>必須是 stateless 或有狀態同步機制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>容量上限&lt;/td>
 &lt;td>單機物理規格上限&lt;/td>
 &lt;td>理論上線性擴展，實際受協調成本限制&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本曲線&lt;/td>
 &lt;td>規格升級非線性（高階機器溢價）&lt;/td>
 &lt;td>線性，但每臺要付 baseline 成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障代價&lt;/td>
 &lt;td>單點失敗影響整個服務&lt;/td>
 &lt;td>一臺壞了還有其他臺、可分流&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>變更節奏&lt;/td>
 &lt;td>變更要停機或 failover、頻率低&lt;/td>
 &lt;td>隨時可加減、頻率高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適合場景&lt;/td>
 &lt;td>資料庫主節點、stateful 服務、單點計算&lt;/td>
 &lt;td>API、worker、無狀態服務&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>讀者要從「程式假設」這欄反推自己的選項。如果服務本身是 stateful（資料庫、cache、session store），水平擴展需要設計 partitioning 或 replication；如果是 stateless API server，水平擴展幾乎可以無腦複製。把這個前提搞錯，就會用水平擴展的策略去動 stateful 服務、然後撞牆。&lt;/p>
&lt;h3 id="第三軸拆功能--拆-partitionakf-scale-cube-y--z-軸">第三軸：拆功能 / 拆 partition（AKF Scale Cube Y / Z 軸）&lt;/h3>
&lt;p>兩個軸的對比把擴展簡化成 capacity scaling 的雙軸、但 AKF Scale Cube 模型提了第三軸：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>X 軸（複製 / 水平擴展）&lt;/strong>：本表 scale-out 即此軸、適合 stateless 服務&lt;/li>
&lt;li>&lt;strong>Y 軸（functional decomposition）&lt;/strong>：沿業務邊界拆服務、跟 &lt;a href="https://tarrragon.github.io/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分&lt;/a> 對應、適合處理「不同功能的擴展需求差距大」&lt;/li>
&lt;li>&lt;strong>Z 軸（data partition / sharding）&lt;/strong>：沿資料拆 partition、適合處理「stateful 服務超出單機容量」&lt;/li>
&lt;/ul>
&lt;p>實務系統常同時動兩到三軸：API 走 X 軸水平、按業務拆 Y 軸（user service / order service / payment service）、user service 內部再用 user ID hash 做 Z 軸 sharding。本章焦點在 X 軸、但讀者規劃容量時要記住 Y / Z 軸是同時可用的工具。&lt;/p>
&lt;h2 id="stateless-是水平擴展的前提">Stateless 是水平擴展的前提&lt;/h2>
&lt;p>Stateless 的核心定義是「處理一個請求不依賴前一個請求留下的本機狀態」。Session、本機快取、檔案系統暫存都會破壞 stateless 假設。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>狀態類型&lt;/th>
 &lt;th>是否破壞 stateless&lt;/th>
 &lt;th>緩解方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Session 存本機&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>把 session 搬到外部 store（Redis、DB），改用 token 認證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>上傳檔案存本機&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>改用物件儲存（S3、GCS）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本機快取&lt;/td>
 &lt;td>視情境&lt;/td>
 &lt;td>共用快取可接受（每臺 cache 各自 build）；強一致快取要外接&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>WebSocket 長連線&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session&lt;/a> 或外部 broker（Pub/Sub、Redis）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>本機 cron / 排程&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>改用分散式排程（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/leader-election/" data-link-title="Leader Election" data-link-desc="從一群對等節點中選出單一主節點負責獨佔工作、leader 失效時自動選新 leader">leader election&lt;/a> 或外部排程服務）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨請求的記憶體狀態&lt;/td>
 &lt;td>破壞&lt;/td>
 &lt;td>移到外部 state store&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>很多人以為自己的服務是 stateless、但一上水平擴展就出事，原因常常在這張表的某一行。判讀方式：把單一機器停掉、重新分配流量到其他機器，使用者體驗是否完全無感？如果有任何「重新登入」「上傳消失」「資料看不到」的情境，就有 stateful 殘留。&lt;/p>
&lt;p>這張表覆蓋顯式狀態。&lt;strong>隱式狀態&lt;/strong>（implicit state）是另一類常被忽略的破壞 stateless 因素：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>In-flight request state&lt;/strong>：HTTP/2 stream、gRPC bidirectional stream — 跨多個請求保持的連線級狀態&lt;/li>
&lt;li>&lt;strong>TLS session resumption&lt;/strong>：session ticket 跟 session ID cache 跨連線、若不集中存會降低重連性能&lt;/li>
&lt;li>&lt;strong>Rate limiter state&lt;/strong>：per-user token bucket、滑動視窗 — 看似無狀態的 middleware 其實在記每個 user 的計數&lt;/li>
&lt;li>&lt;strong>連線預熱（connection warm-up）&lt;/strong>：HTTP/2 / gRPC 連線建立成本高、機器接到流量後需要時間熱起來&lt;/li>
&lt;/ul>
&lt;p>這類「看似 stateless 但有 implicit state」是水平擴展撞牆的常見主因。處理方式是把隱式狀態抽到外部 store（rate limit 用 Redis、TLS session 用共用 cache）或設計連線級 sticky。&lt;/p></description><content:encoded><![CDATA[<p>「要換更大的機器、還是要加更多臺機器？」這個問題在規模成長過程中會反覆出現。垂直擴展（scale-up）與水平擴展（scale-out）對應不同壓力來源、各自承擔不同代價：垂直擴展用「換更大的機器」換取簡單、水平擴展用「加更多機器」換取彈性。規劃容量時先判讀自己的壓力屬於哪一種、再選對應的擴展軸 — 選錯軸的代價會在事故時放大。</p>
<h2 id="兩個軸的責任差異">兩個軸的責任差異</h2>
<p>垂直擴展指把單一機器換成更高規格（更多 CPU / 記憶體 / IOPS），水平擴展指增加機器數量。同樣是「加資源」，兩者面對的工程問題完全不同。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>垂直擴展（scale-up）</th>
          <th>水平擴展（scale-out）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>操作單位</td>
          <td>換一臺機器</td>
          <td>加 N 臺機器</td>
      </tr>
      <tr>
          <td>程式假設</td>
          <td>不需要改</td>
          <td>必須是 stateless 或有狀態同步機制</td>
      </tr>
      <tr>
          <td>容量上限</td>
          <td>單機物理規格上限</td>
          <td>理論上線性擴展，實際受協調成本限制</td>
      </tr>
      <tr>
          <td>成本曲線</td>
          <td>規格升級非線性（高階機器溢價）</td>
          <td>線性，但每臺要付 baseline 成本</td>
      </tr>
      <tr>
          <td>故障代價</td>
          <td>單點失敗影響整個服務</td>
          <td>一臺壞了還有其他臺、可分流</td>
      </tr>
      <tr>
          <td>變更節奏</td>
          <td>變更要停機或 failover、頻率低</td>
          <td>隨時可加減、頻率高</td>
      </tr>
      <tr>
          <td>適合場景</td>
          <td>資料庫主節點、stateful 服務、單點計算</td>
          <td>API、worker、無狀態服務</td>
      </tr>
  </tbody>
</table>
<p>讀者要從「程式假設」這欄反推自己的選項。如果服務本身是 stateful（資料庫、cache、session store），水平擴展需要設計 partitioning 或 replication；如果是 stateless API server，水平擴展幾乎可以無腦複製。把這個前提搞錯，就會用水平擴展的策略去動 stateful 服務、然後撞牆。</p>
<h3 id="第三軸拆功能--拆-partitionakf-scale-cube-y--z-軸">第三軸：拆功能 / 拆 partition（AKF Scale Cube Y / Z 軸）</h3>
<p>兩個軸的對比把擴展簡化成 capacity scaling 的雙軸、但 AKF Scale Cube 模型提了第三軸：</p>
<ul>
<li><strong>X 軸（複製 / 水平擴展）</strong>：本表 scale-out 即此軸、適合 stateless 服務</li>
<li><strong>Y 軸（functional decomposition）</strong>：沿業務邊界拆服務、跟 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 對應、適合處理「不同功能的擴展需求差距大」</li>
<li><strong>Z 軸（data partition / sharding）</strong>：沿資料拆 partition、適合處理「stateful 服務超出單機容量」</li>
</ul>
<p>實務系統常同時動兩到三軸：API 走 X 軸水平、按業務拆 Y 軸（user service / order service / payment service）、user service 內部再用 user ID hash 做 Z 軸 sharding。本章焦點在 X 軸、但讀者規劃容量時要記住 Y / Z 軸是同時可用的工具。</p>
<h2 id="stateless-是水平擴展的前提">Stateless 是水平擴展的前提</h2>
<p>Stateless 的核心定義是「處理一個請求不依賴前一個請求留下的本機狀態」。Session、本機快取、檔案系統暫存都會破壞 stateless 假設。</p>
<table>
  <thead>
      <tr>
          <th>狀態類型</th>
          <th>是否破壞 stateless</th>
          <th>緩解方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Session 存本機</td>
          <td>破壞</td>
          <td>把 session 搬到外部 store（Redis、DB），改用 token 認證</td>
      </tr>
      <tr>
          <td>上傳檔案存本機</td>
          <td>破壞</td>
          <td>改用物件儲存（S3、GCS）</td>
      </tr>
      <tr>
          <td>本機快取</td>
          <td>視情境</td>
          <td>共用快取可接受（每臺 cache 各自 build）；強一致快取要外接</td>
      </tr>
      <tr>
          <td>WebSocket 長連線</td>
          <td>破壞</td>
          <td>用 <a href="/blog/backend/knowledge-cards/sticky-session/" data-link-title="Sticky Session" data-link-desc="說明同一 client 如何在一段時間內持續命中同一個後端實例">sticky session</a> 或外部 broker（Pub/Sub、Redis）</td>
      </tr>
      <tr>
          <td>本機 cron / 排程</td>
          <td>破壞</td>
          <td>改用分散式排程（<a href="/blog/backend/knowledge-cards/leader-election/" data-link-title="Leader Election" data-link-desc="從一群對等節點中選出單一主節點負責獨佔工作、leader 失效時自動選新 leader">leader election</a> 或外部排程服務）</td>
      </tr>
      <tr>
          <td>跨請求的記憶體狀態</td>
          <td>破壞</td>
          <td>移到外部 state store</td>
      </tr>
  </tbody>
</table>
<p>很多人以為自己的服務是 stateless、但一上水平擴展就出事，原因常常在這張表的某一行。判讀方式：把單一機器停掉、重新分配流量到其他機器，使用者體驗是否完全無感？如果有任何「重新登入」「上傳消失」「資料看不到」的情境，就有 stateful 殘留。</p>
<p>這張表覆蓋顯式狀態。<strong>隱式狀態</strong>（implicit state）是另一類常被忽略的破壞 stateless 因素：</p>
<ul>
<li><strong>In-flight request state</strong>：HTTP/2 stream、gRPC bidirectional stream — 跨多個請求保持的連線級狀態</li>
<li><strong>TLS session resumption</strong>：session ticket 跟 session ID cache 跨連線、若不集中存會降低重連性能</li>
<li><strong>Rate limiter state</strong>：per-user token bucket、滑動視窗 — 看似無狀態的 middleware 其實在記每個 user 的計數</li>
<li><strong>連線預熱（connection warm-up）</strong>：HTTP/2 / gRPC 連線建立成本高、機器接到流量後需要時間熱起來</li>
</ul>
<p>這類「看似 stateless 但有 implicit state」是水平擴展撞牆的常見主因。處理方式是把隱式狀態抽到外部 store（rate limit 用 Redis、TLS session 用共用 cache）或設計連線級 sticky。</p>
<h2 id="auto-scaling-的操作模型">Auto Scaling 的操作模型</h2>
<p>水平擴展通常搭配 <a href="/blog/backend/knowledge-cards/autoscaling/" data-link-title="Autoscaling" data-link-desc="說明系統如何依負載自動調整服務實例數量">auto scaling</a> — 根據訊號自動加減機器數量。常見的擴展訊號跟對應的判讀重點：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>反應速度</th>
          <th>判讀重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU 使用率</td>
          <td>中</td>
          <td>通用、但對 I/O bound 服務失準</td>
      </tr>
      <tr>
          <td>記憶體使用率</td>
          <td>慢</td>
          <td>適合判 leak、不適合判尖峰流量</td>
      </tr>
      <tr>
          <td>Request rate (RPS)</td>
          <td>快</td>
          <td>適合 API 服務、需要設定 cool-down 避免抖動</td>
      </tr>
      <tr>
          <td>Queue depth</td>
          <td>快</td>
          <td>適合 worker 服務、queue 是天然 buffer</td>
      </tr>
      <tr>
          <td>Latency P95</td>
          <td>中</td>
          <td>用戶體驗訊號、但已經出現延遲才擴展可能來不及</td>
      </tr>
      <tr>
          <td>自訂業務訊號</td>
          <td>視訊號</td>
          <td>訂單數、活動人數，貼近業務但要自己維護 metric pipeline</td>
      </tr>
  </tbody>
</table>
<p>設定 auto scaling 的判讀順序：先選訊號（CPU vs RPS vs queue depth），再設閾值（避免過早觸發或過晚觸發），最後加 cool-down（避免反覆擴縮造成抖動）。三步驟有一步沒做好就會撞牆。</p>
<p>Auto scaling 不是萬靈丹。三類問題它無法解決：擴展速度跟不上（冷啟動時間視 stack 範圍 5-300 秒、流量尖峰若集中在秒級就來不及）、預測式流量（黑五、新片上線、活動）、stateful 服務（資料庫不能用 auto scaling 加 primary）。這三類要分別用 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>、<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a> 跟 partitioning 處理。</p>
<h2 id="垂直擴展的天花板">垂直擴展的天花板</h2>
<p>垂直擴展看起來簡單但有兩道牆。</p>
<p>第一道是物理上限。雲端機型的最大規格是有限的：以 2025 年公開資料為例、AWS 的 u 系列 instance（如 <code>u7i-12tb</code>、<code>u-24tb1.metal</code>）可達 24 TiB 記憶體級別、vCPU 數量視 SKU 而異；GCP / Azure 也有對應的 memory-optimized 系列、但具體上限隨年份更新。要查最新規格走 vendor 官方文件、不要拿這裡數字當決策依據。對 stateful workload（例如 OLTP 主節點）真實天花板通常出現在 32-64 vCPU 級別、是 lock contention / context switch / memory bandwidth 等架構因素而非規格上限。</p>
<p>第二道是成本曲線。雲端機型的價格不是線性的、越高階的機型每單位資源越貴。以 AWS general-purpose 機型（m 系列）為例、4 vCPU → 8 vCPU 約 ×1.8、8 → 16 約 ×1.9（接近線性）、但到 48 vCPU 以上會明顯偏離線性外推、特別是 memory-optimized（r 系列）跟 high-memory（x 系列）的高階規格溢價更陡。具體曲線依機型 family 跟雲廠商而異 — 走 vendor calculator 算實際 workload 的成本曲線比抓單一倍數可靠。垂直擴展到一定規模、就算物理上撐得住、財務上也會比水平擴展貴。</p>
<p>對 stateful 服務（特別是主資料庫），垂直擴展常常是第一選擇，因為水平擴展需要重新設計 partitioning。但要清楚兩道牆會在什麼時候撞上：基於目前流量增長率，預估垂直擴展能撐多久？多久之後必須改成水平擴展？這個答案要在「還沒撞牆時」就準備好，不是等到下一次撞牆才開始討論。</p>
<h2 id="水平擴展的隱性成本">水平擴展的隱性成本</h2>
<p>水平擴展看起來彈性、但有它自己的代價。</p>
<p><strong>協調成本</strong>：多臺機器要處理「誰是 leader、誰來執行排程、誰來處理同一筆訂單」這類問題。<a href="/blog/backend/knowledge-cards/consensus-protocol/" data-link-title="Consensus Protocol" data-link-desc="讓多個獨立節點在訊息可能延遲、丟失、亂序的網路下對單一決策達成一致的演算法">consensus protocol</a> 跟 <a href="/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">distributed lock</a>（含 leader election、Raft / Paxos 演算法）都會引入新的故障模式跟 latency 代價。</p>
<p><strong>連線池放大</strong>：100 臺機器、每臺對資料庫開 10 個連線，等於對 DB 開 1000 個連線。DB 連線是有限資源，水平擴展應用層的同時要評估資料層連線壓力。常見緩解：connection pooler（PgBouncer）、serverless DB（DynamoDB）、讀寫分離。</p>
<p><strong>狀態同步成本</strong>：cache、session、配置這些「跨機器需要一致」的狀態，要靠外部 store 或 broadcast 機制同步。同步延遲跟頻率會反過來影響服務行為。</p>
<p><strong>Cold start</strong>：新機器啟動到接流量需要時間（image pull、init container、warm-up）。auto scaling 觸發跟流量到達之間的延遲就是這段。冷啟動長的服務（JVM、需要載入大量資料的服務）要預留更多 buffer。</p>
<p><strong>Debug 變難</strong>：請求散落在多臺機器，排查問題需要 log 聚合、trace context。沒有這些基礎設施，水平擴展只會把「一臺機器壞」的問題變成「不知道哪一臺機器壞」的問題。</p>
<h2 id="混合策略">混合策略</h2>
<p>純垂直或純水平在實際系統中都罕見。常見的混合模式：</p>
<ul>
<li><strong>小規模垂直、大規模水平</strong>：早期單機就能撐，先用較大規格降低運維複雜度；流量上來後再轉水平，把每臺機器規格降回中等。</li>
<li><strong>stateless 水平、stateful 垂直</strong>：API server 水平擴展、資料庫主節點垂直擴展、加 read replica 做讀路徑水平擴展。</li>
<li><strong>熱資料水平 sharding、冷資料保持單庫</strong>：把熱表用 partition key 拆到多個 shard，冷表保留在主庫不動。</li>
<li><strong>核心服務垂直保底、邊緣服務水平彈性</strong>：核心交易服務用更大規格降低事故風險，前端、推薦等服務走 auto scaling。</li>
</ul>
<p>選混合策略時，要明確標記每個服務在哪個軸上、極限在哪、下一步轉換點在什麼條件下觸發。沒有這張對照表，混合策略容易變成「每個服務都是特例」、最後沒人記得當初為什麼這樣設計。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>加機器後 QPS 沒提升</td>
          <td>stateful 殘留（本機快取 / session / 鎖）</td>
          <td>找出 stateful 點、移到外部 store，或改回垂直擴展</td>
      </tr>
      <tr>
          <td>加機器後 DB 連線爆掉</td>
          <td>連線池放大、DB 是瓶頸</td>
          <td>加 connection pooler、評估讀寫分離、考慮資料層擴展</td>
      </tr>
      <tr>
          <td>Auto scaling 反覆擴縮</td>
          <td>cool-down 太短或訊號抖動</td>
          <td>加 cool-down、改用更穩定訊號（移動平均、business metric）</td>
      </tr>
      <tr>
          <td>流量尖峰時新機器來不及啟動</td>
          <td>cold start 太長 / 預測訊號不夠早</td>
          <td>改 <a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">scheduled scaling</a> 或 <a href="/blog/backend/knowledge-cards/predictive-scaling/" data-link-title="Predictive Scaling" data-link-desc="說明用歷史模式或 ML 模型預測流量、提前擴容的 autoscaler 模式">predictive scaling</a>、warm pool</td>
      </tr>
      <tr>
          <td>垂直擴展後成本曲線陡升</td>
          <td>撞到高階機型溢價</td>
          <td>評估水平擴展轉型 / 重構 stateful 部分</td>
      </tr>
      <tr>
          <td>水平擴展後事故 MTTR 拉長</td>
          <td>觀測能力跟不上</td>
          <td>補 trace context、結構化 log、service topology</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把「加機器」當作所有效能問題的萬靈丹。如果瓶頸在演算法、SQL query、序列化、locks，加機器只會讓問題變得更貴。先用 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a> 確定瓶頸位置，再決定擴展軸。</p>
<p>把 auto scaling 當成「設定完就不用管」。auto scaling 是 reactive 策略，它無法處理可預期的尖峰（活動、新片上線、節日）。預期型流量要用 scheduled / predictive scaling 提前準備。</p>
<p>把 stateless 當成「沒有狀態就好」。WebSocket、long-polling、上傳、檔案處理這類服務天然 stateful、強行水平擴展會出事。要分辨「業務本質 stateful」跟「實作偷懶 stateful」，前者用 partitioning 處理、後者用重構移除。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「擴展軸的選擇與前提」。當問題進入具體量化（要加多少臺機器？headroom 多少？），交給 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>；進入瓶頸定位（瓶頸在哪一層？），交給 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a>；進入服務拆分（要不要先把 stateful 部分拆出來再水平擴展？），交給 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>擴展軸選擇可用以下案例回寫。每個案例對應的軸不同，引用時要先辨識案例的主要壓力來源，再對照本章相應段落。</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — 案例主軸是「stateless API 層水平擴展、stateful 資料層改用 DynamoDB 移除單點」，直接對應本章「stateless 是水平擴展的前提」段。是本批最貼近 scaling axis 主題的案例。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理</a> — 案例展示水平擴展到極端規模後，協調成本（cluster 治理、版本一致性）變成新的瓶頸；對照本章「水平擴展的隱性成本 / 協調成本」段。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom：DynamoDB + EKS 上的遊戲後端</a> — 案例主軸是 KV 業務語意、不是 scaling axis 取捨；但可反向追問「stateful 玩家狀態為何適合 KV vs RDB」、對照本章「stateless 是水平擴展的前提」段中的「狀態類型 vs 緩解方向」表。</li>
<li><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：把關聯式 DB 統一到 Aurora</a> — 案例主軸是「DB 種類整併」、不直接對應 scale-up vs scale-out；但 Aurora 在 single-primary 規格選擇上隱含了「先垂直、再考慮分散」的策略，可作為「垂直擴展天花板」段的對照組。</li>
</ul>
<p>Zomato 跟 Netflix 不在這份案例清單裡的原因要先講清楚：擴展軸的真實示範案例在後端教材中相對稀缺、09 模組多數案例的主軸落在 vendor 或容量規劃。Zoom 是這四個案例中最貼近教科書 — stateless API 水平 + stateful 改用 DynamoDB 的組合直接示範本章核心。Riot Games 揭示水平到極端規模後協調成本翻轉成新瓶頸。Capcom 跟 Netflix Aurora 不直接示範擴展軸取捨、但用反向追問「為什麼選 KV / 為什麼 single-primary 仍是 default」能把它們的決策放回擴展軸框架。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/09-performance-capacity/performance-theory/" data-link-title="9.1 壓測理論與系統行為" data-link-desc="Little&#39;s Law、queueing theory、USL、saturation curve 在容量規劃中的角色">9.1 壓測理論與系統行為</a> 的交接：USL 跟 Little&rsquo;s Law 在理論上推導水平擴展的曲線、本章解釋這道牆在運維現場長什麼樣。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：擴展軸選定後，容量規劃決定具體數字。</li>
<li>與 <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分</a> 的交接：水平擴展常常是服務拆分的觸發點，反之亦然。</li>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">01 database high-concurrency-access</a> 的交接：資料層水平擴展（sharding、replica）的具體機制。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 應用層查詢反模式與 Query 預算</a></strong>：選定擴展軸後、在加機器前先用反模式清單收回單機可撐住的容量。</p>
<p>其他延伸方向：</p>
<ul>
<li>容量計算與 headroom 模型 → <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
<li>擴展前的瓶頸定位 → <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>服務拆分如何配合水平擴展 → <a href="/blog/backend/10-system-evolution/service-decomposition-boundaries/" data-link-title="10.1 服務拆分與邊界判讀" data-link-desc="整理 monolith vs microservice 取捨、服務邊界判讀訊號、拆分時機與回退路徑">10.1 服務拆分與邊界判讀</a></li>
</ul>
]]></content:encoded></item><item><title>9.14 連線池放大解法（PgBouncer / RDS Proxy / ProxySQL）</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/connection-pool-amplification/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提&lt;/a> 指出了水平擴展應用層時的隱性成本之一：連線池放大 — 100 臺機器 × 每臺 10 個連線 = 對 DB 開 1000 個連線、超過 PostgreSQL &lt;code>max_connections&lt;/code> default（100）十倍。本章把這條撞牆訊號的具體解法說清楚 — connection pooler 是什麼、PgBouncer / RDS Proxy / ProxySQL 怎麼選、不同場景的取捨。&lt;/p>
&lt;h2 id="連線池放大的物理本質">連線池放大的物理本質&lt;/h2>
&lt;p>PostgreSQL / MySQL 每個連線都會在 DB server 端配一個 backend process / thread。Backend 佔 5-15 MB 記憶體、context switch 也有成本。當應用層連線數超過 DB 機器能負擔的數量，會出現三類問題：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>記憶體吃光&lt;/strong>：500 個 backend × 10 MB = 5 GB、再加 shared buffer、可能直接 OOM&lt;/li>
&lt;li>&lt;strong>Context switch 抖動&lt;/strong>：上百個 backend 競爭 CPU、上下文切換 overhead 變成主要消耗&lt;/li>
&lt;li>&lt;strong>連線建立失敗&lt;/strong>：超過 &lt;code>max_connections&lt;/code> 後、新請求拿不到連線、即使現有連線多數 idle&lt;/li>
&lt;/ul>
&lt;p>問題的根因不是「連線多」、是「連線&lt;strong>生命週期跟使用率不對齊&lt;/strong>」。應用層 connection pool 通常維持「每臺機器 N 個常駐連線、避免每個 request 重新建連」、但 100 臺機器各自 keep 10 個常駐就是 1000 個 idle 連線。&lt;/p>
&lt;p>解法的方向不是「砍應用層連線數」（會讓 connection acquisition 變慢、影響 latency）、是「在 DB 跟應用層之間放一層 multiplexer」— 把多個應用層連線複用到少數 DB 連線上。這層中介就是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/connection-pooler/" data-link-title="Connection Pooler" data-link-desc="應用層跟資料庫之間的連線複用中介層、解水平擴展時的連線數放大問題">connection pooler&lt;/a>。&lt;/p>
&lt;h2 id="connection-pooler-三大選項">Connection Pooler 三大選項&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>部署模式&lt;/th>
 &lt;th>主要適用 DB&lt;/th>
 &lt;th>主要特點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>PgBouncer&lt;/td>
 &lt;td>Self-managed / sidecar&lt;/td>
 &lt;td>PostgreSQL only&lt;/td>
 &lt;td>輕量（C 寫的 single process）、三種 pooling 模式可選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>AWS RDS Proxy&lt;/td>
 &lt;td>Managed&lt;/td>
 &lt;td>RDS / Aurora (PG / MySQL)&lt;/td>
 &lt;td>整合 IAM auth、自動 failover、計價 per vCPU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ProxySQL&lt;/td>
 &lt;td>Self-managed&lt;/td>
 &lt;td>MySQL&lt;/td>
 &lt;td>規則型 routing、可做 query rewriting、自動 failover&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="pgbouncer--三種-pooling-模式決定一切">PgBouncer — 三種 pooling 模式決定一切&lt;/h3>
&lt;p>PgBouncer 的核心參數是 &lt;code>pool_mode&lt;/code>：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Session mode&lt;/strong>：應用層 client 拿到的連線、跟 DB backend 1:1 綁定、整個 session 結束才釋放。其實沒做 multiplexing、只是 connection caching。&lt;/li>
&lt;li>&lt;strong>Transaction mode&lt;/strong>：每個 transaction 結束、應用層 client 的連線釋放回 pool、下個 transaction 再分配 DB backend。multiplexing 比較強、但&lt;strong>不支援 transaction-scoped state&lt;/strong>（如 &lt;code>SET LOCAL&lt;/code>、prepared statement、temporary table）。&lt;/li>
&lt;li>&lt;strong>Statement mode&lt;/strong>：每個 statement 結束就釋放、最強 multiplexing 但&lt;strong>不支援 transaction&lt;/strong>。極少用、只在純 stateless query workload 適用。&lt;/li>
&lt;/ul>
&lt;p>Transaction mode 是多數場景的 default。但要注意：應用層的 ORM / driver 可能默認用 prepared statement、跟 transaction mode 衝突。PostgreSQL 14+ 的 protocol-level prepared statement 才相容、JDBC / asyncpg 等需要特別配置。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a> 指出了水平擴展應用層時的隱性成本之一：連線池放大 — 100 臺機器 × 每臺 10 個連線 = 對 DB 開 1000 個連線、超過 PostgreSQL <code>max_connections</code> default（100）十倍。本章把這條撞牆訊號的具體解法說清楚 — connection pooler 是什麼、PgBouncer / RDS Proxy / ProxySQL 怎麼選、不同場景的取捨。</p>
<h2 id="連線池放大的物理本質">連線池放大的物理本質</h2>
<p>PostgreSQL / MySQL 每個連線都會在 DB server 端配一個 backend process / thread。Backend 佔 5-15 MB 記憶體、context switch 也有成本。當應用層連線數超過 DB 機器能負擔的數量，會出現三類問題：</p>
<ul>
<li><strong>記憶體吃光</strong>：500 個 backend × 10 MB = 5 GB、再加 shared buffer、可能直接 OOM</li>
<li><strong>Context switch 抖動</strong>：上百個 backend 競爭 CPU、上下文切換 overhead 變成主要消耗</li>
<li><strong>連線建立失敗</strong>：超過 <code>max_connections</code> 後、新請求拿不到連線、即使現有連線多數 idle</li>
</ul>
<p>問題的根因不是「連線多」、是「連線<strong>生命週期跟使用率不對齊</strong>」。應用層 connection pool 通常維持「每臺機器 N 個常駐連線、避免每個 request 重新建連」、但 100 臺機器各自 keep 10 個常駐就是 1000 個 idle 連線。</p>
<p>解法的方向不是「砍應用層連線數」（會讓 connection acquisition 變慢、影響 latency）、是「在 DB 跟應用層之間放一層 multiplexer」— 把多個應用層連線複用到少數 DB 連線上。這層中介就是 <a href="/blog/backend/knowledge-cards/connection-pooler/" data-link-title="Connection Pooler" data-link-desc="應用層跟資料庫之間的連線複用中介層、解水平擴展時的連線數放大問題">connection pooler</a>。</p>
<h2 id="connection-pooler-三大選項">Connection Pooler 三大選項</h2>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>部署模式</th>
          <th>主要適用 DB</th>
          <th>主要特點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PgBouncer</td>
          <td>Self-managed / sidecar</td>
          <td>PostgreSQL only</td>
          <td>輕量（C 寫的 single process）、三種 pooling 模式可選</td>
      </tr>
      <tr>
          <td>AWS RDS Proxy</td>
          <td>Managed</td>
          <td>RDS / Aurora (PG / MySQL)</td>
          <td>整合 IAM auth、自動 failover、計價 per vCPU</td>
      </tr>
      <tr>
          <td>ProxySQL</td>
          <td>Self-managed</td>
          <td>MySQL</td>
          <td>規則型 routing、可做 query rewriting、自動 failover</td>
      </tr>
  </tbody>
</table>
<h3 id="pgbouncer--三種-pooling-模式決定一切">PgBouncer — 三種 pooling 模式決定一切</h3>
<p>PgBouncer 的核心參數是 <code>pool_mode</code>：</p>
<ul>
<li><strong>Session mode</strong>：應用層 client 拿到的連線、跟 DB backend 1:1 綁定、整個 session 結束才釋放。其實沒做 multiplexing、只是 connection caching。</li>
<li><strong>Transaction mode</strong>：每個 transaction 結束、應用層 client 的連線釋放回 pool、下個 transaction 再分配 DB backend。multiplexing 比較強、但<strong>不支援 transaction-scoped state</strong>（如 <code>SET LOCAL</code>、prepared statement、temporary table）。</li>
<li><strong>Statement mode</strong>：每個 statement 結束就釋放、最強 multiplexing 但<strong>不支援 transaction</strong>。極少用、只在純 stateless query workload 適用。</li>
</ul>
<p>Transaction mode 是多數場景的 default。但要注意：應用層的 ORM / driver 可能默認用 prepared statement、跟 transaction mode 衝突。PostgreSQL 14+ 的 protocol-level prepared statement 才相容、JDBC / asyncpg 等需要特別配置。</p>
<h3 id="aws-rds-proxy--managed-換掉運維">AWS RDS Proxy — managed 換掉運維</h3>
<p>RDS Proxy 是 PgBouncer / ProxySQL 同類功能的 managed 版本：AWS 負責部署、HA、failover、IAM 整合。應用層連到 RDS Proxy endpoint、Proxy 在背後維持跟 RDS / Aurora 的連線池。</p>
<p>特點：</p>
<ul>
<li><strong>連線 share 模式類似 transaction mode</strong>：自動 detect 連線是否在 transaction、空閒時釋放</li>
<li><strong>IAM auth 整合</strong>：應用層用 IAM token、不用維護 DB password</li>
<li><strong>Failover 加速</strong>：DB failover 時 Proxy 維持應用層連線不斷、background 重連 new primary。Failover 期間應用層感受最小化。</li>
<li><strong>計價</strong>：per vCPU-hour、Aurora 約 $0.015/vCPU-hr、RDS 約 $0.02/vCPU-hr — 加在 RDS 計價上面</li>
</ul>
<p>不適用場景：很多 read-only / analytics workload 不需要 connection pooler、純讀 replica 直接連通常更便宜。RDS Proxy 是給「寫入混合」「連線抖動嚴重」這類場景。</p>
<h3 id="proxysql--mysql-規則型-routing">ProxySQL — MySQL 規則型 routing</h3>
<p>ProxySQL 是 MySQL 生態的 connection pooler、但比 PgBouncer 更全功能：</p>
<ul>
<li><strong>Query routing rules</strong>：可以按 query pattern 把 query 導去不同 backend（讀路徑去 replica、寫路徑去 primary、特定 query 強制 cache）</li>
<li><strong>Connection multiplexing</strong>：類似 PgBouncer transaction mode</li>
<li><strong>Query rewriting</strong>：可以攔截 query 改寫（debug / 漸進遷移 schema）</li>
<li><strong>Auto failover</strong>：監控 backend 健康、自動切流</li>
</ul>
<p>ProxySQL 的代價是學習曲線跟運維成本 — 規則設計需要對 query pattern 跟 DB topology 有掌控、設錯規則會把 query 導去錯誤 backend、debug 困難。</p>
<h2 id="選型對照">選型對照</h2>
<p>實務選型的關鍵變數是「DB 廠商 / managed 程度 / 規模 / 預算」：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>推薦</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>AWS RDS / Aurora、團隊不想自管</td>
          <td>RDS Proxy</td>
          <td>Managed、整合度高、failover 加速是 free value</td>
      </tr>
      <tr>
          <td>AWS RDS / Aurora、需要極致省成本</td>
          <td>PgBouncer（PG）/ ProxySQL（MySQL）on EC2</td>
          <td>比 RDS Proxy 便宜、但要自管 HA</td>
      </tr>
      <tr>
          <td>GCP Cloud SQL / 自管 PostgreSQL</td>
          <td>PgBouncer</td>
          <td>PG 生態事實標準、配置文件多</td>
      </tr>
      <tr>
          <td>Azure Database for PostgreSQL</td>
          <td>PgBouncer 或 Azure 內建 connection pooling</td>
          <td>Azure 部分 SKU 內建類似功能、檢查 vendor 文件</td>
      </tr>
      <tr>
          <td>MySQL 需要讀寫分離 + query routing</td>
          <td>ProxySQL</td>
          <td>規則型 routing 是 ProxySQL 強項</td>
      </tr>
      <tr>
          <td>不確定要不要 connection pooler</td>
          <td>先用 vendor 內建（RDS Proxy / PG managed pooler）跑一段、再評估自管</td>
          <td>降低初期決策成本</td>
      </tr>
  </tbody>
</table>
<h2 id="不裝-pooler-的判讀">不裝 pooler 的判讀</h2>
<p>Connection pooler 不是必要 — 在以下情境可以暫時不裝：</p>
<ul>
<li><strong>應用層機器數 &lt; 10</strong>：對 DB 連線總數壓力小、deferred 安裝 pooler 沒問題</li>
<li><strong>每臺機器連線數 &lt; 5</strong>：應用層 connection pool 已經很省、再加 pooler 改善有限</li>
<li><strong>DB 機器規格大、<code>max_connections</code> 充裕</strong>：高階 RDS instance 可開到 5000-10000 連線、有 buffer 之前不必加 pooler</li>
<li><strong>Workload 全是長 transaction</strong>：transaction mode pooler 在這種 workload 跟 session mode 沒差、收益低</li>
</ul>
<p>該裝 pooler 的訊號是相反：應用層機器數 ≥ 20、每臺連線數 ≥ 10、<code>max_connections</code> 使用率 ≥ 70%、或 P99 connection wait time 升高。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DB <code>pg_stat_activity</code> 顯示大量 idle 連線</td>
          <td>應用層 keep-alive 連線、實際使用率低</td>
          <td>加 connection pooler 把 idle 釋放回 DB</td>
      </tr>
      <tr>
          <td>應用層 connection acquisition 等待時間升高</td>
          <td>應用層 pool 太小、或 DB 連線數已撞 <code>max_connections</code></td>
          <td>加 pooler 把連線總數壓低、應用層 pool size 維持原樣</td>
      </tr>
      <tr>
          <td>DB failover 後應用層 5-10 分鐘錯誤率高</td>
          <td>應用層 connection pool 沒 detect 到 backend 切換</td>
          <td>RDS Proxy 的 failover 加速、或應用層 connection validation 加強</td>
      </tr>
      <tr>
          <td>Pooler 上線後出現「unexpected error」</td>
          <td>transaction mode 跟 prepared statement / SET LOCAL 衝突</td>
          <td>改 ORM 配置、用 protocol-level prepared statement 或避開 SET LOCAL</td>
      </tr>
      <tr>
          <td>應用層 N+1 query 仍然存在</td>
          <td>Pooler 沒解 N+1、它只解連線數放大</td>
          <td>回 <a href="/blog/backend/01-database/query-anti-patterns/" data-link-title="1.13 應用層查詢反模式與 Query 預算" data-link-desc="整理 N&#43;1、select *、缺索引、ORM lazy load、long transaction 等查詢反模式與每請求的 query 預算判讀">1.13 query 反模式</a> 修反模式</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 connection pooler 當「N+1 解藥」。Pooler 解的是「連線數放大」、不是「query 數量過多」。N+1 query 在裝完 pooler 後仍然慢、只是 DB 不會因為連線爆掉而當機。兩個是正交問題、各自要解。</p>
<p>把 RDS Proxy 當「免費功能」。Proxy 的計價跟 RDS / Aurora 本體疊加、高 connection volume 場景 Proxy 成本可能可觀。要算實際的 cost-per-request、不是預設「managed 一定值得」。</p>
<p>把 transaction mode 配置當「裝完就好」。Prepared statement / SET LOCAL / temporary table 都會跟 transaction mode 衝突、ORM 預設行為要 audit 過、不然會在 production 出現難 debug 的「query 隨機失敗」。</p>
<h2 id="定位邊界">定位邊界</h2>
<p>本章專注「連線池放大的解法」。當問題進入擴展軸選擇（要垂直 vs 水平？stateful 前提？）、回 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a>；進入 DB 本身的容量規劃（要多大規格 instance？要不要 read replica？）、進 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a>；進入 application-level connection 設計（per-request pool / persistent pool）、進 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL</a>。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>09 案例庫多數案例規模到 connection pool 已是 secondary concern、但兩個案例有對應參考：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/zoom-covid-surge-dynamodb/" data-link-title="9.C18 Zoom：COVID 期間從 1000 萬到 3 億 DAU 的 30 倍突發" data-link-desc="Zoom 在 2020 年 COVID 爆發時、日活從 1000 萬衝到 3 億、用 DynamoDB 撐住會議後端">9.C18 Zoom：COVID 30 倍突發</a> — Zoom 把 stateful 資料層改用 DynamoDB、繞過 SQL connection pool 問題（KV 沒有 backend process 概念）。對照本章可問：若 Zoom 保留 SQL、connection pool 怎麼設計才撐得住 30 倍突發？</li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash：CockroachDB 多主寫入</a> — DoorDash 從 Aurora single-primary 換成 CockroachDB 多主、connection pool 設計從「集中在 primary」變成「分散在多 node」。對照本章可問：CockroachDB 是否仍需要 connection pooler？</li>
</ul>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸</a> 的交接：9.13 提出隱性成本、本章給具體解法。</li>
<li>與 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL 讀寫邊界</a> 的交接：1.1 講應用層 connection pool 設計、本章補 DB 端 pooler 中介層。</li>
<li>與 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">01 vendors</a> 的交接：各 DB vendor 的內建 pooler 能力詳見 vendor deep article。</li>
<li>與 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃</a> 的交接：pooler 加上後、DB 容量規劃的單位從「連線數」變成「DB backend 數 + Pooler vCPU」。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看擴展軸選擇的完整 framing、回 <a href="/blog/backend/09-performance-capacity/scaling-axes/" data-link-title="9.13 擴展軸與 Stateless 前提" data-link-desc="整理垂直 / 水平擴展取捨、stateless vs stateful 前提、auto scaling 操作模型與兩種擴展的 hidden cost">9.13 擴展軸與 Stateless 前提</a>。要看 DB-side 高併發處理、進 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發 SQL 讀寫邊界</a>。要看具體 vendor 的 pooler 文件、進對應 <a href="/blog/backend/01-database/vendors/" data-link-title="資料庫 Vendor 清單" data-link-desc="規劃 SQL、managed SQL、document、KV 與 distributed SQL 的服務頁撰寫順序與教學大綱">vendor deep article</a>。</p>
]]></content:encoded></item></channel></rss>