<?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>Stateless on Tarragon</title><link>https://tarrragon.github.io/blog/tags/stateless/</link><description>Recent content in Stateless on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/stateless/index.xml" rel="self" type="application/rss+xml"/><item><title>模組二：水平擴展</title><link>https://tarrragon.github.io/blog/devops/02-horizontal-scaling/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/02-horizontal-scaling/</guid><description>&lt;p>回答「怎麼從一個實例變成多個實例」。水平擴展的前提是服務 stateless — 每個實例可以獨立處理任何請求。&lt;/p>
&lt;h2 id="待寫章節">待寫章節&lt;/h2>
&lt;ul>
&lt;li>&lt;input disabled="" type="checkbox"> Stateless 設計原則（狀態放 DB / cache / 外部儲存、不放 process memory）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Session 處理（sticky session / session store / JWT stateless）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> Shared storage 的選型（NFS / S3 / DB — 不同 workload 的適合方案）&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 擴展的觸發訊號和縮回條件&lt;/li>
&lt;li>&lt;input disabled="" type="checkbox"> 垂直擴展 vs 水平擴展的判斷（什麼時候加 CPU、什麼時候加實例）&lt;/li>
&lt;/ul>
&lt;h2 id="跨分類引用">跨分類引用&lt;/h2>
&lt;ul>
&lt;li>← &lt;a href="https://tarrragon.github.io/blog/devops/01-load-balancing/" data-link-title="模組一：負載平衡與反向代理" data-link-desc="流量進來怎麼分給多個服務實例 — nginx / HAProxy / DNS round-robin 的選型和健康檢查路由設計">devops 模組一 負載平衡&lt;/a>：LB 是水平擴展的前提&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">monitoring 模組四 Collector&lt;/a>：Collector 的 stateless 設計讓多實例可行&lt;/li>
&lt;li>→ &lt;a href="https://tarrragon.github.io/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 資料庫&lt;/a>：Shared storage 的 DB 選型&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>回答「怎麼從一個實例變成多個實例」。水平擴展的前提是服務 stateless — 每個實例可以獨立處理任何請求。</p>
<h2 id="待寫章節">待寫章節</h2>
<ul>
<li><input disabled="" type="checkbox"> Stateless 設計原則（狀態放 DB / cache / 外部儲存、不放 process memory）</li>
<li><input disabled="" type="checkbox"> Session 處理（sticky session / session store / JWT stateless）</li>
<li><input disabled="" type="checkbox"> Shared storage 的選型（NFS / S3 / DB — 不同 workload 的適合方案）</li>
<li><input disabled="" type="checkbox"> 擴展的觸發訊號和縮回條件</li>
<li><input disabled="" type="checkbox"> 垂直擴展 vs 水平擴展的判斷（什麼時候加 CPU、什麼時候加實例）</li>
</ul>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>← <a href="/blog/devops/01-load-balancing/" data-link-title="模組一：負載平衡與反向代理" data-link-desc="流量進來怎麼分給多個服務實例 — nginx / HAProxy / DNS round-robin 的選型和健康檢查路由設計">devops 模組一 負載平衡</a>：LB 是水平擴展的前提</li>
<li>→ <a href="/blog/monitoring/04-collector/" data-link-title="模組四：Collector 設計" data-link-desc="收 → 驗 → 存 → 查 → 觸發的完整鏈路 — Go 單一 binary、可插拔 Storage Backend、rule engine">monitoring 模組四 Collector</a>：Collector 的 stateless 設計讓多實例可行</li>
<li>→ <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">backend 資料庫</a>：Shared storage 的 DB 選型</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></channel></rss>