<?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/devops/operations/05-capacity-planning/</link><description>Recent content in 模組五：容量規劃與壓力測試 on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/index.xml" rel="self" type="application/rss+xml"/><item><title>流量模型建立</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/</guid><description>&lt;p>流量模型是容量規劃的輸入：它把「這個服務會收到什麼樣的流量」量化成幾個可測量的維度，後面的峰值估算、壓測、資源規格與成本，全部從這個模型推出。模型建錯，後面每一步都在錯誤的前提上算。所以容量規劃的第一步是先把流量長什麼樣描述清楚，而不是急著估要開幾台機器。&lt;/p>
&lt;p>一個完整的流量模型至少量五個維度：平均與峰值的比值、時間上的分布、用戶的分布、操作的分布、以及突發的型態。少量任何一維，模型就會在某個情境下失準——最常見的失準是「模型撐得住、production 撐不住」，那幾乎都是漏了某個維度。&lt;/p>
&lt;h2 id="峰均比是工程意義最大的單一指標">峰均比是工程意義最大的單一指標&lt;/h2>
&lt;p>峰均比（peak/average ratio）是峰值流量除以平均流量，它一個數字就決定了容量要按平均規劃還是按峰值規劃。峰均比 1.5 左右的服務流量平緩，容量可以接近平均值來配、不必大幅超額；峰均比 3 到 5 的服務是 bursty 的，必須按峰值規劃，代價是平日大幅超額配置、多數時間資源閒置。&lt;/p>
&lt;p>這兩種型態的差距在真實服務裡很極端。一個大型電商的公開大促案例，當天 24 小時收 1.67 億請求、峰值 3500 RPS，峰均比約 1.8——算溫和型，按峰值配一點超額就穩。搶票型服務則是另一個極端：整批票 5 分鐘賣完，峰值全集中在開賣的瞬間，峰均比比平緩型高出一個數量級以上，這種流量沒辦法靠「平均值加係數」規劃，得專門為那 5 分鐘設計。峰均比先算出來，才知道自己在哪一端。&lt;/p>
&lt;h2 id="同一秒內怎麼到達比每秒平均更決定壓力">同一秒內怎麼到達，比每秒平均更決定壓力&lt;/h2>
&lt;p>平均 RPS 只說了「每秒幾個請求」，沒說這些請求在那一秒內怎麼分布。同樣是每秒 1000 個請求，均勻到達（接近 Poisson）跟集中在幾十毫秒內爆發（bursty）對系統的壓力完全不同——bursty 的到達會在瞬間打滿連線池、塞爆佇列，即使每秒平均看起來還在容量內。&lt;/p>
&lt;p>所以流量模型要標到達型態，不能只記平均 RPS。bursty 的來源在真實服務裡很具體：一則推播同時喚醒幾百萬 app、一個 KOL 的推廣連結、一部新片上架、一則突發新聞。這些都會製造「平均看起來沒事、瞬間打爆」的流量。模型漏掉到達型態，壓測時用均勻流量測、上線遇到 bursty 就垮。&lt;/p>
&lt;h2 id="操作分布決定容量往哪裡堆">操作分布決定容量往哪裡堆&lt;/h2>
&lt;p>讀寫比直接改變容量該往哪個資源堆。90% 讀的服務跟 50% 讀的服務，容量設計完全不同——讀重的可以靠快取擋掉大部分負載，寫重的擋不了快取、壓力直接落在儲存層的 IOPS。同樣的總 RPS，讀重的瓶頸在快取容量、寫重的瓶頸在磁碟寫入，規劃的方向相反。&lt;/p>
&lt;p>操作分布還要看端點的組合（endpoint mix）跟請求大小的分布。不同端點的成本差幾個數量級，把它們當成等重的請求來估容量會嚴重失準；請求大小（payload size）的分布則影響網路頻寬與序列化成本，要按端點、按用戶分層各自算 percentile，而不是用一個平均值蓋過去。&lt;/p>
&lt;h2 id="從-production-log-抽出可信的模型">從 production log 抽出可信的模型&lt;/h2>
&lt;p>流量模型的來源是真實流量，不是憑感覺估的。從 production 抽模型分四步：先蒐集資料（從 access log、APM trace、metric 系統採樣，涵蓋至少一個完整的週期——含平日、週末、峰谷），再分組統計（按端點、按租戶分組，各算 percentile、標到達型態、算 payload 分布），接著做序列重播（保留請求之間的到達間隔，讓 burst 在壓測時能重現，而不只是灌一個平均 RPS），最後脫敏（雜湊加鹽、但保持資料的 cardinality——不同值的數量、例如有幾個不同的用戶 ID——跟 production 一致，否則快取命中率會失真）。&lt;/p>
&lt;p>抽出來的模型要驗證。拿模型去壓測，比對四類指標跟 production 是否吻合：吞吐型態（總 RPS 加端點組合）、延遲分布（p50/p95/p99）、資源使用率、錯誤與重試型態。若模型在測試環境撐得住、production 卻撐不住，代表漏了維度，要回到蒐集階段補——這個「模型過關但實境失守」的偏差，是流量模型最該防的失準。&lt;/p>
&lt;h2 id="用-littles-law-把流量翻成並發">用 Little&amp;rsquo;s Law 把流量翻成並發&lt;/h2>
&lt;p>流量模型的一個直接用途是反推系統要準備多少並發容量，工具是 Little&amp;rsquo;s Law：&lt;code>L = λ × W&lt;/code>，並發數等於到達率乘以每個請求停留的時間。這條關係在穩態下恆成立、不需要任何分布假設。舉例：到達率 1000 RPS、每個請求平均停留 200 毫秒，穩態並發就是 &lt;code>1000 × 0.2 = 200&lt;/code>——這個 200 直接對應要準備多大的連線池、執行緒池或非同步 worker 數。&lt;/p>
&lt;p>反過來用也成立：一個卡在固定大小的連線池（L）加上已知的處理延遲（W），可以算出它最多支撐多少 RPS。Little&amp;rsquo;s Law 讓「流量」跟「資源規格」之間有一條可計算的橋，不必靠猜。&lt;/p>
&lt;h2 id="模型會過時要定期重抽">模型會過時，要定期重抽&lt;/h2>
&lt;p>流量模型是某個時間點的快照，會隨服務演進失效。失效的訊號很具體：新功能改變了用戶動線、進入新市場改變了用戶組成（例如打進行動優先的市場、mobile 佔比大增）、一場行銷活動改變了 burst 型態、或用戶習慣的長期轉變（遠距工作改變了平日與週末的比例）。&lt;/p>
&lt;p>最劇烈的失效是結構性的流量位移——一個視訊服務在疫情期間流量漲到平時的 30 倍，且不會回落，baseline 永久上移。這種情況舊模型整個作廢，要以新的 baseline 重抽，而不是在舊模型上加係數。維護的節奏是每季 review 一次、遇到重大改動立即重抽。模型不維護，容量規劃就是拿過期的流量在算未來的資源。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>把峰值從模型推出來、加多少安全係數 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>&lt;/li>
&lt;li>用壓測工具驗證這個模型撐不撐得住 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法&lt;/a>&lt;/li>
&lt;li>上游的效能理論與 workload modeling 完整方法 → &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 翻成可重播的壓測模型">backend 效能容量&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>流量模型是容量規劃的輸入：它把「這個服務會收到什麼樣的流量」量化成幾個可測量的維度，後面的峰值估算、壓測、資源規格與成本，全部從這個模型推出。模型建錯，後面每一步都在錯誤的前提上算。所以容量規劃的第一步是先把流量長什麼樣描述清楚，而不是急著估要開幾台機器。</p>
<p>一個完整的流量模型至少量五個維度：平均與峰值的比值、時間上的分布、用戶的分布、操作的分布、以及突發的型態。少量任何一維，模型就會在某個情境下失準——最常見的失準是「模型撐得住、production 撐不住」，那幾乎都是漏了某個維度。</p>
<h2 id="峰均比是工程意義最大的單一指標">峰均比是工程意義最大的單一指標</h2>
<p>峰均比（peak/average ratio）是峰值流量除以平均流量，它一個數字就決定了容量要按平均規劃還是按峰值規劃。峰均比 1.5 左右的服務流量平緩，容量可以接近平均值來配、不必大幅超額；峰均比 3 到 5 的服務是 bursty 的，必須按峰值規劃，代價是平日大幅超額配置、多數時間資源閒置。</p>
<p>這兩種型態的差距在真實服務裡很極端。一個大型電商的公開大促案例，當天 24 小時收 1.67 億請求、峰值 3500 RPS，峰均比約 1.8——算溫和型，按峰值配一點超額就穩。搶票型服務則是另一個極端：整批票 5 分鐘賣完，峰值全集中在開賣的瞬間，峰均比比平緩型高出一個數量級以上，這種流量沒辦法靠「平均值加係數」規劃，得專門為那 5 分鐘設計。峰均比先算出來，才知道自己在哪一端。</p>
<h2 id="同一秒內怎麼到達比每秒平均更決定壓力">同一秒內怎麼到達，比每秒平均更決定壓力</h2>
<p>平均 RPS 只說了「每秒幾個請求」，沒說這些請求在那一秒內怎麼分布。同樣是每秒 1000 個請求，均勻到達（接近 Poisson）跟集中在幾十毫秒內爆發（bursty）對系統的壓力完全不同——bursty 的到達會在瞬間打滿連線池、塞爆佇列，即使每秒平均看起來還在容量內。</p>
<p>所以流量模型要標到達型態，不能只記平均 RPS。bursty 的來源在真實服務裡很具體：一則推播同時喚醒幾百萬 app、一個 KOL 的推廣連結、一部新片上架、一則突發新聞。這些都會製造「平均看起來沒事、瞬間打爆」的流量。模型漏掉到達型態，壓測時用均勻流量測、上線遇到 bursty 就垮。</p>
<h2 id="操作分布決定容量往哪裡堆">操作分布決定容量往哪裡堆</h2>
<p>讀寫比直接改變容量該往哪個資源堆。90% 讀的服務跟 50% 讀的服務，容量設計完全不同——讀重的可以靠快取擋掉大部分負載，寫重的擋不了快取、壓力直接落在儲存層的 IOPS。同樣的總 RPS，讀重的瓶頸在快取容量、寫重的瓶頸在磁碟寫入，規劃的方向相反。</p>
<p>操作分布還要看端點的組合（endpoint mix）跟請求大小的分布。不同端點的成本差幾個數量級，把它們當成等重的請求來估容量會嚴重失準；請求大小（payload size）的分布則影響網路頻寬與序列化成本，要按端點、按用戶分層各自算 percentile，而不是用一個平均值蓋過去。</p>
<h2 id="從-production-log-抽出可信的模型">從 production log 抽出可信的模型</h2>
<p>流量模型的來源是真實流量，不是憑感覺估的。從 production 抽模型分四步：先蒐集資料（從 access log、APM trace、metric 系統採樣，涵蓋至少一個完整的週期——含平日、週末、峰谷），再分組統計（按端點、按租戶分組，各算 percentile、標到達型態、算 payload 分布），接著做序列重播（保留請求之間的到達間隔，讓 burst 在壓測時能重現，而不只是灌一個平均 RPS），最後脫敏（雜湊加鹽、但保持資料的 cardinality——不同值的數量、例如有幾個不同的用戶 ID——跟 production 一致，否則快取命中率會失真）。</p>
<p>抽出來的模型要驗證。拿模型去壓測，比對四類指標跟 production 是否吻合：吞吐型態（總 RPS 加端點組合）、延遲分布（p50/p95/p99）、資源使用率、錯誤與重試型態。若模型在測試環境撐得住、production 卻撐不住，代表漏了維度，要回到蒐集階段補——這個「模型過關但實境失守」的偏差，是流量模型最該防的失準。</p>
<h2 id="用-littles-law-把流量翻成並發">用 Little&rsquo;s Law 把流量翻成並發</h2>
<p>流量模型的一個直接用途是反推系統要準備多少並發容量，工具是 Little&rsquo;s Law：<code>L = λ × W</code>，並發數等於到達率乘以每個請求停留的時間。這條關係在穩態下恆成立、不需要任何分布假設。舉例：到達率 1000 RPS、每個請求平均停留 200 毫秒，穩態並發就是 <code>1000 × 0.2 = 200</code>——這個 200 直接對應要準備多大的連線池、執行緒池或非同步 worker 數。</p>
<p>反過來用也成立：一個卡在固定大小的連線池（L）加上已知的處理延遲（W），可以算出它最多支撐多少 RPS。Little&rsquo;s Law 讓「流量」跟「資源規格」之間有一條可計算的橋，不必靠猜。</p>
<h2 id="模型會過時要定期重抽">模型會過時，要定期重抽</h2>
<p>流量模型是某個時間點的快照，會隨服務演進失效。失效的訊號很具體：新功能改變了用戶動線、進入新市場改變了用戶組成（例如打進行動優先的市場、mobile 佔比大增）、一場行銷活動改變了 burst 型態、或用戶習慣的長期轉變（遠距工作改變了平日與週末的比例）。</p>
<p>最劇烈的失效是結構性的流量位移——一個視訊服務在疫情期間流量漲到平時的 30 倍，且不會回落，baseline 永久上移。這種情況舊模型整個作廢，要以新的 baseline 重抽，而不是在舊模型上加係數。維護的節奏是每季 review 一次、遇到重大改動立即重抽。模型不維護，容量規劃就是拿過期的流量在算未來的資源。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>把峰值從模型推出來、加多少安全係數 → <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>用壓測工具驗證這個模型撐不撐得住 → <a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a></li>
<li>上游的效能理論與 workload modeling 完整方法 → <a href="/blog/backend/09-performance-capacity/workload-modeling/" data-link-title="9.2 Workload Modeling" data-link-desc="把 production traffic shape 翻成可重播的壓測模型">backend 效能容量</a></li>
</ul>
]]></content:encoded></item><item><title>峰值估算</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/</guid><description>&lt;p>峰值估算的第一步是分清這個峰值屬於哪一種形狀，因為不同形狀的倍率推法完全不同。一個提前三個月就知道的年度大促，跟一個賽事進球瞬間的爆量，跟一個產品爆紅後持續數週的高原，估算方法各不一樣——用同一套方法估所有峰值，是估算失準的起點。分好類，才知道這個峰值該用歷史倍率推、還是該按最壞情況預備。&lt;/p>
&lt;p>峰值大致分五種形狀：可預期的極端峰值（年度大促、體育決賽，提前數月已知）、事件型的不可預期峰值（賽事進球、突發新聞、KOL 帶貨）、瞬間爆量（搶購型，開賣即爆、幾十分鐘結束）、產品爆紅的持續高原（隨熱度漲跌）、以及結構性位移（外部衝擊讓 baseline 永久上移）。前兩種是本章倍率估算的主要對象，後三種各有專屬的預備方式。&lt;/p>
&lt;h2 id="用歷史倍率階梯推可預期峰值">用歷史倍率階梯推可預期峰值&lt;/h2>
&lt;p>可預期的峰值有歷史資料可依，估算方法是拿一個穩定的 baseline，乘上該事件的歷史倍率。倍率是隨事件等級變化的階梯，不是一個固定數字。一個體育博彩服務的公開案例，以賽季的日常流量為 baseline，季後賽升到 2 到 3 倍、冠軍賽 4 到 5 倍、最高級別的單場賽事（如超級盃）5 到 10 倍——每一級對應不同的準備強度。估算時要先定位「這次事件是哪一級」，才知道套哪一格倍率。&lt;/p>
&lt;p>倍率階梯的可信度來自每次事件後的校準。每辦完一次，回頭比對這次的實際峰值跟事前的預測差多少、當時的資源使用率峰值到哪、headroom 是不是留得剛好。這個事後校準（回顧）把下一次的倍率估得更準——沒有校準的倍率階梯，用幾次就會跟真實流量脫節。真正提前的估算通常在事件前約 90 天定案：確認預期的峰值倍數、確認 headroom 比例、確認跨區域與跨可用區的分布，讓容量計畫有時間落地。容量計畫之後的 quota 申請、壓測、故障演練與預先擴容各排在事件前幾天、為什麼排在那裡，見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備&lt;/a>〈T-90 → T-0 準備時程〉。&lt;/p>
&lt;h2 id="事件型峰值按最壞情況預備">事件型峰值按最壞情況預備&lt;/h2>
&lt;p>事件型的不可預期峰值沒有精確的發生時間，只有「一旦發生會多猛」的量級。一個賽事的進球、一則突發新聞，可以讓流量在幾秒內衝到平均的 10 到 50 倍。這種峰值估算的目標是「來的時候有多大的量級」，並讓系統隨時能承受那個量級——因為它可能在賽事的任何一分鐘發生，估「什麼時候來」沒有意義。&lt;/p>
&lt;p>這類峰值的預備常常不能只靠事後擴容，因為爆發是秒級的、而擴容有冷啟動延遲。可行的做法是事前把容量預先拉到能接住那個量級，或搭配預測式的擴容（在賽事的高風險時段提前擴），這牽涉到擴縮的時機判斷，在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷&lt;/a> 展開。這裡要確立的是：事件型峰值的估算目標是量級、不是時間點。&lt;/p>
&lt;h2 id="安全係數的數學根據在飽和曲線">安全係數的數學根據在飽和曲線&lt;/h2>
&lt;p>峰值估算之外要再加一層安全係數，它有明確的數學根據——對應系統飽和曲線上「knee 以下的 headroom」，不是單純為了保守。一個健康的系統應該運轉在利用率 50% 到 70% 之間，留出的那段 headroom 就是安全係數要覆蓋的：它同時吸收 burst 的瞬間波動、跟峰值估算本身的誤差。自動擴容的目標利用率常設在 60% 到 70%，正是這條曲線推導出的安全位置。&lt;/p>
&lt;p>安全係數不能對所有系統套同一個數，因為膝點的位置隨系統類型變。無狀態服務、有鎖競爭的資料庫、吃磁碟 I/O 的佇列，膝點高低不同，有狀態的關鍵系統膝點來得早、要留的 headroom 比無狀態服務厚。飽和曲線的三段區間、排隊理論的量化門檻、以及各類系統膝點的實際位置，在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷&lt;/a> 完整展開；這裡用它來解釋安全係數為什麼存在、為什麼不能一律套同一個數。&lt;/p>
&lt;h2 id="從業務指標反推各層峰值">從業務指標反推各層峰值&lt;/h2>
&lt;p>峰值估算不只估總量，成熟的做法是從業務指標往下反推每一層的峰值需求。從用戶感知的端到端延遲目標（例如 p99 500 毫秒）出發，把這個預算拆給路徑上的每一段，各段的延遲配額再配上預期的 RPS、經 Little&amp;rsquo;s Law 算出各段要準備的並發，並發再翻成實例數、連線池大小、快取容量。這條反推鏈讓峰值估算不停在「總 RPS 是多少」，而是落到每一層的具體規格。&lt;/p>
&lt;p>反推過程本身會暴露設計問題。如果某一段算出來的容量超過了它所用元件（例如某個 vendor 服務）的上限，或某段分到的延遲配額短到連跨可用區的網路往返（至少 1 到 2 毫秒）都容不下，代表這個峰值目標在當前架構下達不到，要回頭改架構而不是硬估一個數。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>峰值估算的輸入——流量模型怎麼建 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立&lt;/a>&lt;/li>
&lt;li>安全係數背後的飽和曲線、膝點怎麼找 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷&lt;/a>&lt;/li>
&lt;li>用壓測驗證系統撐不撐得住估出來的峰值 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法&lt;/a>&lt;/li>
&lt;li>從業務預估換算成擴容規格與預算、交給決策者與會計，以及活動後怎麼校準集中比例、每人請求數、每台處理量與單位請求成本 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準&lt;/a>&lt;/li>
&lt;li>突發流量的即時應對（估不到的爆量怎麼撐）→ &lt;a href="https://tarrragon.github.io/blog/devops/operations/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動、新聞曝光或 error storm 讓送進 collector 的事件量暴增時：突發分類、降級策略、queue 緩衝、規模分級應對">模組七 突發流量&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>峰值估算的第一步是分清這個峰值屬於哪一種形狀，因為不同形狀的倍率推法完全不同。一個提前三個月就知道的年度大促，跟一個賽事進球瞬間的爆量，跟一個產品爆紅後持續數週的高原，估算方法各不一樣——用同一套方法估所有峰值，是估算失準的起點。分好類，才知道這個峰值該用歷史倍率推、還是該按最壞情況預備。</p>
<p>峰值大致分五種形狀：可預期的極端峰值（年度大促、體育決賽，提前數月已知）、事件型的不可預期峰值（賽事進球、突發新聞、KOL 帶貨）、瞬間爆量（搶購型，開賣即爆、幾十分鐘結束）、產品爆紅的持續高原（隨熱度漲跌）、以及結構性位移（外部衝擊讓 baseline 永久上移）。前兩種是本章倍率估算的主要對象，後三種各有專屬的預備方式。</p>
<h2 id="用歷史倍率階梯推可預期峰值">用歷史倍率階梯推可預期峰值</h2>
<p>可預期的峰值有歷史資料可依，估算方法是拿一個穩定的 baseline，乘上該事件的歷史倍率。倍率是隨事件等級變化的階梯，不是一個固定數字。一個體育博彩服務的公開案例，以賽季的日常流量為 baseline，季後賽升到 2 到 3 倍、冠軍賽 4 到 5 倍、最高級別的單場賽事（如超級盃）5 到 10 倍——每一級對應不同的準備強度。估算時要先定位「這次事件是哪一級」，才知道套哪一格倍率。</p>
<p>倍率階梯的可信度來自每次事件後的校準。每辦完一次，回頭比對這次的實際峰值跟事前的預測差多少、當時的資源使用率峰值到哪、headroom 是不是留得剛好。這個事後校準（回顧）把下一次的倍率估得更準——沒有校準的倍率階梯，用幾次就會跟真實流量脫節。真正提前的估算通常在事件前約 90 天定案：確認預期的峰值倍數、確認 headroom 比例、確認跨區域與跨可用區的分布，讓容量計畫有時間落地。容量計畫之後的 quota 申請、壓測、故障演練與預先擴容各排在事件前幾天、為什麼排在那裡，見 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備</a>〈T-90 → T-0 準備時程〉。</p>
<h2 id="事件型峰值按最壞情況預備">事件型峰值按最壞情況預備</h2>
<p>事件型的不可預期峰值沒有精確的發生時間，只有「一旦發生會多猛」的量級。一個賽事的進球、一則突發新聞，可以讓流量在幾秒內衝到平均的 10 到 50 倍。這種峰值估算的目標是「來的時候有多大的量級」，並讓系統隨時能承受那個量級——因為它可能在賽事的任何一分鐘發生，估「什麼時候來」沒有意義。</p>
<p>這類峰值的預備常常不能只靠事後擴容，因為爆發是秒級的、而擴容有冷啟動延遲。可行的做法是事前把容量預先拉到能接住那個量級，或搭配預測式的擴容（在賽事的高風險時段提前擴），這牽涉到擴縮的時機判斷，在 <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a> 展開。這裡要確立的是：事件型峰值的估算目標是量級、不是時間點。</p>
<h2 id="安全係數的數學根據在飽和曲線">安全係數的數學根據在飽和曲線</h2>
<p>峰值估算之外要再加一層安全係數，它有明確的數學根據——對應系統飽和曲線上「knee 以下的 headroom」，不是單純為了保守。一個健康的系統應該運轉在利用率 50% 到 70% 之間，留出的那段 headroom 就是安全係數要覆蓋的：它同時吸收 burst 的瞬間波動、跟峰值估算本身的誤差。自動擴容的目標利用率常設在 60% 到 70%，正是這條曲線推導出的安全位置。</p>
<p>安全係數不能對所有系統套同一個數，因為膝點的位置隨系統類型變。無狀態服務、有鎖競爭的資料庫、吃磁碟 I/O 的佇列，膝點高低不同，有狀態的關鍵系統膝點來得早、要留的 headroom 比無狀態服務厚。飽和曲線的三段區間、排隊理論的量化門檻、以及各類系統膝點的實際位置，在 <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a> 完整展開；這裡用它來解釋安全係數為什麼存在、為什麼不能一律套同一個數。</p>
<h2 id="從業務指標反推各層峰值">從業務指標反推各層峰值</h2>
<p>峰值估算不只估總量，成熟的做法是從業務指標往下反推每一層的峰值需求。從用戶感知的端到端延遲目標（例如 p99 500 毫秒）出發，把這個預算拆給路徑上的每一段，各段的延遲配額再配上預期的 RPS、經 Little&rsquo;s Law 算出各段要準備的並發，並發再翻成實例數、連線池大小、快取容量。這條反推鏈讓峰值估算不停在「總 RPS 是多少」，而是落到每一層的具體規格。</p>
<p>反推過程本身會暴露設計問題。如果某一段算出來的容量超過了它所用元件（例如某個 vendor 服務）的上限，或某段分到的延遲配額短到連跨可用區的網路往返（至少 1 到 2 毫秒）都容不下，代表這個峰值目標在當前架構下達不到，要回頭改架構而不是硬估一個數。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>峰值估算的輸入——流量模型怎麼建 → <a href="/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a></li>
<li>安全係數背後的飽和曲線、膝點怎麼找 → <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>用壓測驗證系統撐不撐得住估出來的峰值 → <a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a></li>
<li>從業務預估換算成擴容規格與預算、交給決策者與會計，以及活動後怎麼校準集中比例、每人請求數、每台處理量與單位請求成本 → <a href="/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</a></li>
<li>突發流量的即時應對（估不到的爆量怎麼撐）→ <a href="/blog/devops/operations/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動、新聞曝光或 error storm 讓送進 collector 的事件量暴增時：突發分類、降級策略、queue 緩衝、規模分級應對">模組七 突發流量</a></li>
</ul>
]]></content:encoded></item><item><title>壓力測試工具與方法</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/</guid><description>&lt;p>壓測要回答的問題是「這個服務在什麼負載下開始撐不住」，不是「跑一次看會不會掛」。有價值的壓測是階梯加壓、觀察系統行為在哪個點從穩定轉為劣化，把 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型&lt;/a> 的假設拿到真實負載下驗證。工具選對，這個劣化點測得準；工具選錯，測到的是工具自己的極限、不是被測系統的。&lt;/p>
&lt;p>工具選型的核心不是「哪個最強」，是「哪個最貼合本團隊的 workload model 表達能力與 CI 整合需求」。同樣一組流量，能不能用工具複製出來，決定壓測結果可不可信。&lt;/p>
&lt;h2 id="選型看六個維度">選型看六個維度&lt;/h2>
&lt;p>選壓測工具要按六個維度評估，只看「能不能發 HTTP 請求」會選錯：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>腳本表達能力&lt;/strong>：能不能寫完整的用戶動線（登入到瀏覽到結帳），而不只是打單一端點。複雜系統的瓶頸常在跨端點的資源競爭，單端點壓測看不到。&lt;/li>
&lt;li>&lt;strong>協議支援&lt;/strong>：HTTP、WebSocket、gRPC、TCP。現代後端常用 WebSocket 與 gRPC，有些老工具要靠外掛才支援。&lt;/li>
&lt;li>&lt;strong>規模能力&lt;/strong>：單機能發多少 RPS、能不能分散式擴容。單機 wrk 能發一到五萬 RPS，分散式部署的工具能到百萬級。&lt;/li>
&lt;li>&lt;strong>CI 整合&lt;/strong>：能不能在 PR 上跑輕量效能檢查、結果能不能機器可讀、能不能跟 baseline 比對。沒有 CI 整合的工具只能做事件型壓測，做不了持續的效能治理。&lt;/li>
&lt;li>&lt;strong>結果分析&lt;/strong>：原生儀表板、或整進既有的 metric 系統、或純文字輸出。&lt;/li>
&lt;li>&lt;strong>學習曲線&lt;/strong>：腳本語言與團隊熟悉度。工具再好、團隊不會用，就會變成一兩個人的孤島技能。&lt;/li>
&lt;/ul>
&lt;p>主流開源工具的定位大致是：k6（JavaScript 腳本、CI 友善、現代專案首選）、Locust（Python、適合複雜業務邏輯、單機吞吐受限要靠分散式）、Gatling（Scala、報表精美、學習曲線陡）、JMeter（協議廣、腳本是 XML 難版控、已在用的團隊再續用）、wrk 與 Vegeta（單機極限壓測、找天花板用）。選型的完整維度與雲端託管服務見 &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 的工程選型">backend 壓測工具選型&lt;/a>，這一章聚焦怎麼實際跑一次、怎麼讀結果。&lt;/p>
&lt;h2 id="用-k6-實際跑一次">用 k6 實際跑一次&lt;/h2>
&lt;p>k6 的壓測腳本是一段 JavaScript，定義加壓的階段（stages）與通過標準（thresholds）。下面這段腳本把虛擬用戶（VU）在 5 秒內拉到 20、維持 10 秒、再 5 秒降回 0，並設一條「p95 延遲要低於 500 毫秒」的通過線：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-javascript" data-lang="javascript">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="nx">http&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;k6/http&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="kr">import&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">check&lt;/span> &lt;span class="p">}&lt;/span> &lt;span class="nx">from&lt;/span> &lt;span class="s1">&amp;#39;k6&amp;#39;&lt;/span>&lt;span class="p">;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="kr">export&lt;/span> &lt;span class="kr">const&lt;/span> &lt;span class="nx">options&lt;/span> &lt;span class="o">=&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="nx">stages&lt;/span>&lt;span class="o">:&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="p">{&lt;/span> &lt;span class="nx">duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;5s&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&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 class="nx">duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;10s&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">20&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="p">{&lt;/span> &lt;span class="nx">duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="s1">&amp;#39;5s&amp;#39;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nx">target&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="mi">0&lt;/span> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="p">],&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="nx">thresholds&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="nx">http_req_duration&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s1">&amp;#39;p(95)&amp;lt;500&amp;#39;&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">10&lt;/span>&lt;span class="cl">&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="kr">export&lt;/span> &lt;span class="k">default&lt;/span> &lt;span class="kd">function&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">12&lt;/span>&lt;span class="cl"> &lt;span class="kr">const&lt;/span> &lt;span class="nx">res&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">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s1">&amp;#39;http://target/&amp;#39;&lt;/span>&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">check&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="nx">res&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="p">{&lt;/span> &lt;span class="s1">&amp;#39;status is 200&amp;#39;&lt;/span>&lt;span class="o">:&lt;/span> &lt;span class="p">(&lt;/span>&lt;span class="nx">r&lt;/span>&lt;span class="p">)&lt;/span> &lt;span class="p">=&amp;gt;&lt;/span> &lt;span class="nx">r&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="nx">status&lt;/span> &lt;span class="o">===&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">14&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>對一個 nginx 目標實跑這段腳本，輸出的關鍵段落是：&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"> █ THRESHOLDS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> http_req_duration
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ✓ &amp;#39;p(95)&amp;lt;500&amp;#39; p(95)=1.3ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> █ TOTAL RESULTS
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> checks_succeeded...: 100.00% 637011 out of 637011
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> http_req_duration..: avg=403µs min=11µs med=126µs max=562ms p(90)=749µs p(95)=1.3ms
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> http_req_failed....: 0.00% 0 out of 637011
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl"> http_reqs..........: 637011 30971/s&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀這份輸出的順序是：先看 &lt;code>THRESHOLDS&lt;/code> 有沒有通過（打勾代表 p95 通過了 500 毫秒的線），再看 &lt;code>http_req_failed&lt;/code> 確認錯誤率（0% 代表沒有失敗請求），接著看 &lt;code>http_reqs&lt;/code> 的每秒數（這次是 30971 RPS）。真正的判讀重點在 &lt;code>http_req_duration&lt;/code> 那一行的分布：平均 403 微秒沒什麼資訊量，要看的是 p90、p95 跟 max 之間的差距。這次 p95 是 1.3 毫秒、max 卻是 562 毫秒——絕大多數請求很快，但有極少數慢了三個數量級。這種「平均漂亮、尾巴很長」正是只看平均會漏掉的問題，也是壓測要看 percentile 而非平均的理由。&lt;/p>
&lt;p>這次測出的 sub-millisecond 延遲有一個前提：壓測機跟被測的 nginx 跑在同一台主機的容器網路裡，沒有真實的網路往返。這正好示範了下一節第一個反模式——壓測機跟被測機太近，延遲會被嚴重低估、p99 比 production 樂觀。&lt;/p>
&lt;h2 id="wrk-找單機天花板">wrk 找單機天花板&lt;/h2>
&lt;p>wrk 的定位跟 k6 不同：它不寫複雜動線，專門對單一端點灌到極限、找這個服務的天花板。saturation discovery 的第一輪常用它——&lt;code>wrk -t4 -c100 -d30s --latency http://target/&lt;/code> 開 4 個執行緒、100 條連線、壓 30 秒，回報 RPS 與延遲分布。k6 適合驗證「這條用戶動線在預期負載下的行為」，wrk 適合回答「這個端點的絕對上限在哪」，兩者常搭配：wrk 先找到天花板的量級，k6 再在天花板以下驗證真實動線。&lt;/p></description><content:encoded><![CDATA[<p>壓測要回答的問題是「這個服務在什麼負載下開始撐不住」，不是「跑一次看會不會掛」。有價值的壓測是階梯加壓、觀察系統行為在哪個點從穩定轉為劣化，把 <a href="/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型</a> 的假設拿到真實負載下驗證。工具選對，這個劣化點測得準；工具選錯，測到的是工具自己的極限、不是被測系統的。</p>
<p>工具選型的核心不是「哪個最強」，是「哪個最貼合本團隊的 workload model 表達能力與 CI 整合需求」。同樣一組流量，能不能用工具複製出來，決定壓測結果可不可信。</p>
<h2 id="選型看六個維度">選型看六個維度</h2>
<p>選壓測工具要按六個維度評估，只看「能不能發 HTTP 請求」會選錯：</p>
<ul>
<li><strong>腳本表達能力</strong>：能不能寫完整的用戶動線（登入到瀏覽到結帳），而不只是打單一端點。複雜系統的瓶頸常在跨端點的資源競爭，單端點壓測看不到。</li>
<li><strong>協議支援</strong>：HTTP、WebSocket、gRPC、TCP。現代後端常用 WebSocket 與 gRPC，有些老工具要靠外掛才支援。</li>
<li><strong>規模能力</strong>：單機能發多少 RPS、能不能分散式擴容。單機 wrk 能發一到五萬 RPS，分散式部署的工具能到百萬級。</li>
<li><strong>CI 整合</strong>：能不能在 PR 上跑輕量效能檢查、結果能不能機器可讀、能不能跟 baseline 比對。沒有 CI 整合的工具只能做事件型壓測，做不了持續的效能治理。</li>
<li><strong>結果分析</strong>：原生儀表板、或整進既有的 metric 系統、或純文字輸出。</li>
<li><strong>學習曲線</strong>：腳本語言與團隊熟悉度。工具再好、團隊不會用，就會變成一兩個人的孤島技能。</li>
</ul>
<p>主流開源工具的定位大致是：k6（JavaScript 腳本、CI 友善、現代專案首選）、Locust（Python、適合複雜業務邏輯、單機吞吐受限要靠分散式）、Gatling（Scala、報表精美、學習曲線陡）、JMeter（協議廣、腳本是 XML 難版控、已在用的團隊再續用）、wrk 與 Vegeta（單機極限壓測、找天花板用）。選型的完整維度與雲端託管服務見 <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 的工程選型">backend 壓測工具選型</a>，這一章聚焦怎麼實際跑一次、怎麼讀結果。</p>
<h2 id="用-k6-實際跑一次">用 k6 實際跑一次</h2>
<p>k6 的壓測腳本是一段 JavaScript，定義加壓的階段（stages）與通過標準（thresholds）。下面這段腳本把虛擬用戶（VU）在 5 秒內拉到 20、維持 10 秒、再 5 秒降回 0，並設一條「p95 延遲要低於 500 毫秒」的通過線：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kr">import</span> <span class="nx">http</span> <span class="nx">from</span> <span class="s1">&#39;k6/http&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kr">import</span> <span class="p">{</span> <span class="nx">check</span> <span class="p">}</span> <span class="nx">from</span> <span class="s1">&#39;k6&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">options</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="nx">stages</span><span class="o">:</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="p">{</span> <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5s&#39;</span><span class="p">,</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">20</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="p">{</span> <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;10s&#39;</span><span class="p">,</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">20</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="p">{</span> <span class="nx">duration</span><span class="o">:</span> <span class="s1">&#39;5s&#39;</span><span class="p">,</span> <span class="nx">target</span><span class="o">:</span> <span class="mi">0</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  <span class="p">],</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  <span class="nx">thresholds</span><span class="o">:</span> <span class="p">{</span> <span class="nx">http_req_duration</span><span class="o">:</span> <span class="p">[</span><span class="s1">&#39;p(95)&lt;500&#39;</span><span class="p">]</span> <span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="kr">export</span> <span class="k">default</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">  <span class="kr">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="nx">http</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;http://target/&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="nx">check</span><span class="p">(</span><span class="nx">res</span><span class="p">,</span> <span class="p">{</span> <span class="s1">&#39;status is 200&#39;</span><span class="o">:</span> <span class="p">(</span><span class="nx">r</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nx">r</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span> <span class="p">});</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>對一個 nginx 目標實跑這段腳本，輸出的關鍵段落是：</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">  █ THRESHOLDS
</span></span><span class="line"><span class="ln">2</span><span class="cl">    http_req_duration
</span></span><span class="line"><span class="ln">3</span><span class="cl">    ✓ &#39;p(95)&lt;500&#39; p(95)=1.3ms
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">  █ TOTAL RESULTS
</span></span><span class="line"><span class="ln">6</span><span class="cl">    checks_succeeded...: 100.00% 637011 out of 637011
</span></span><span class="line"><span class="ln">7</span><span class="cl">    http_req_duration..: avg=403µs min=11µs med=126µs max=562ms p(90)=749µs p(95)=1.3ms
</span></span><span class="line"><span class="ln">8</span><span class="cl">    http_req_failed....: 0.00%  0 out of 637011
</span></span><span class="line"><span class="ln">9</span><span class="cl">    http_reqs..........: 637011 30971/s</span></span></code></pre></div><p>讀這份輸出的順序是：先看 <code>THRESHOLDS</code> 有沒有通過（打勾代表 p95 通過了 500 毫秒的線），再看 <code>http_req_failed</code> 確認錯誤率（0% 代表沒有失敗請求），接著看 <code>http_reqs</code> 的每秒數（這次是 30971 RPS）。真正的判讀重點在 <code>http_req_duration</code> 那一行的分布：平均 403 微秒沒什麼資訊量，要看的是 p90、p95 跟 max 之間的差距。這次 p95 是 1.3 毫秒、max 卻是 562 毫秒——絕大多數請求很快，但有極少數慢了三個數量級。這種「平均漂亮、尾巴很長」正是只看平均會漏掉的問題，也是壓測要看 percentile 而非平均的理由。</p>
<p>這次測出的 sub-millisecond 延遲有一個前提：壓測機跟被測的 nginx 跑在同一台主機的容器網路裡，沒有真實的網路往返。這正好示範了下一節第一個反模式——壓測機跟被測機太近，延遲會被嚴重低估、p99 比 production 樂觀。</p>
<h2 id="wrk-找單機天花板">wrk 找單機天花板</h2>
<p>wrk 的定位跟 k6 不同：它不寫複雜動線，專門對單一端點灌到極限、找這個服務的天花板。saturation discovery 的第一輪常用它——<code>wrk -t4 -c100 -d30s --latency http://target/</code> 開 4 個執行緒、100 條連線、壓 30 秒，回報 RPS 與延遲分布。k6 適合驗證「這條用戶動線在預期負載下的行為」，wrk 適合回答「這個端點的絕對上限在哪」，兩者常搭配：wrk 先找到天花板的量級，k6 再在天花板以下驗證真實動線。</p>
<h2 id="讓結果失真的反模式">讓結果失真的反模式</h2>
<p>壓測最大的風險是測出一個看起來漂亮、但不能外推到 production 的數字。幾個常見的失真來源：</p>
<ul>
<li><strong>只測單一 API、不測用戶動線</strong>：找不到跨端點的資源競爭、也累積不出真實的 session 狀態。</li>
<li><strong>壓測機跟被測機在同一網段</strong>：網路延遲被低估，p99 比 production 樂觀——上一節的 sub-millisecond 結果就是這個例子。</li>
<li><strong>壓測時主機被其他工作負載佔用</strong>：同一個服務在主機安靜時與過載時量到的上限，差的是主機而不是服務；紀錄要附上量測期間主機本身的 load、核心數與可用磁碟，判讀方式見 <a href="/blog/devops/operations/05-capacity-planning/shared-host-contention/" data-link-title="共用主機的資源競爭：容量估算、故障範圍、隔離層級與監控的位置" data-link-desc="多個服務、資料庫與監控放在同一台主機（或同一台實體機上的多台虛擬機）時，資源競爭在容量估算、故障範圍、隔離的做法與監控的位置上各自要考慮什麼：共用發生在哪些層、量到的數字什麼時候能互相比較、哪些服務適合放在一起、從同機加限制到專用實體機的隔離層級與各層的觸發條件">共用主機的資源競爭</a>〈容量估算〉。</li>
<li><strong>壓測時 throttle 到自己的工具</strong>：測出的是工具的極限、不是被測系統的，訊號是壓測機自己的 CPU 先滿。</li>
<li><strong>只看平均延遲</strong>：<a href="/blog/backend/knowledge-cards/tail-latency/" data-link-title="Tail Latency" data-link-desc="說明 p99 / p999 等長尾延遲為何比平均延遲更能反映 saturation">尾延遲</a>看不到，p99 的劣化被平均值蓋掉。</li>
<li><strong>壓測環境跟 production 硬體不一致</strong>：CPU 型號、網路、磁碟 IOPS 差很多，結果不可外推。</li>
<li><strong>跑了壓測卻沒驗證模型</strong>：沒拿結果跟 production metric 比對，不知道壓測用的流量模型貼不貼近真實。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>壓測用的流量模型怎麼建、怎麼驗證 → <a href="/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a></li>
<li>用階梯加壓找飽和點、讀哪些訊號 → <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>工具選型的完整維度、雲端託管壓測、production 流量重播 → <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 的工程選型">backend 壓測工具選型</a></li>
</ul>
]]></content:encoded></item><item><title>規模拐點判斷</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/</guid><description>&lt;p>什麼訊號代表該擴容、什麼代表可以縮容，規模拐點判斷的根本概念是飽和曲線：系統不是「撐得住」跟「掛掉」的二元，中間有一段可測量的劣化區。負載往上加，系統先在延遲平穩的線性區，過了某個點進入延遲開始上升的膝點區（knee），再往上就是延遲不可預測的懸崖區（cliff）。拐點判斷的核心，是在系統進入膝點區、還沒到懸崖時就讀出訊號，而不是等它掉下懸崖。&lt;/p>
&lt;p>判斷標準先建立在利用率的三段區間上。利用率低於 50% 是線性區，延遲平穩；50% 到 80% 是膝點區，延遲開始上升但仍可接受；超過 80% 是懸崖區，延遲不可預測、可能逾時或引發級聯故障。健康的系統應該運轉在 50% 到 70% 之間，這也是 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a> 裡安全係數要覆蓋的那段 headroom。&lt;/p>
&lt;p>這三段區間背後是排隊理論：利用率往高處走，佇列長度增加的幅度越來越陡——50% 到 70% 增加數倍、70% 到 90% 再急遽放大（實際倍數視佇列模型而定，但方向一致是超線性）；延遲跟佇列長度成正比，所以延遲在高利用率區是指數成長。80% 這個懸崖門檻是通用的形狀，膝點的實際位置隨系統類型變——無狀態服務的膝點大約在 80% CPU、有鎖競爭的資料庫大約 60%、吃磁碟 I/O 的佇列服務可能 50% 就到。這就是為什麼安全係數不能對所有系統套同一個數：有狀態的關鍵系統膝點來得早，要留的 headroom 比無狀態服務厚。判斷一個系統該在多少利用率就準備擴容，要看它是哪一類、膝點在哪，不是一律套 80%。&lt;/p>
&lt;h2 id="膝點的早期訊號p99-先於平均劣化">膝點的早期訊號：p99 先於平均劣化&lt;/h2>
&lt;p>膝點的價值在於它是「該擴容」的早期訊號，但它只在對的指標上看得見。四個訊號共同指向系統正在進入膝點區：吞吐量從線性成長轉為次線性（加壓上去、吞吐不再等比增加）、p50 還穩但 p99 與 p999 開始飆、佇列開始堆積（不只是利用率上升）、而錯誤率仍接近零。最後這條很關鍵——膝點區的系統還沒開始報錯，只看錯誤率會以為一切正常，要看尾延遲才抓得到。&lt;/p>
&lt;p>尾延遲的敏感度是這裡的判讀核心。p50 對垃圾回收暫停、重試風暴、長尾都不敏感，純看平均或 p50 會誤判「飽和還沒到」；p99 對連線競爭敏感，能較早看到飽和的苗頭；p999 對垃圾回收的 stop-the-world、leader 選舉、重試風暴最敏感。看容量該不該擴，看的是 p99 跟 p999 的曲線有沒有開始上翹，不是平均值。等到吞吐量開始下降、p99 變成逾時、錯誤率飆升、重試風暴出現，那已經是掉進懸崖區了，擴容是在救火而不是預防。懸崖區裡服務呈現哪一種結果，由先耗盡的資源與等待是否設有上限決定，見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現&lt;/a>。&lt;/p>
&lt;h2 id="用-ramp-up-找膝點不用單點壓測">用 ramp-up 找膝點，不用單點壓測&lt;/h2>
&lt;p>膝點的位置要靠階梯加壓（ramp-up）找，不能用固定的單一壓力值測。按 1 倍、2 倍、4 倍、8 倍逐級加壓，每一級維持 5 到 10 分鐘看穩態行為，才能看出曲線在哪個負載開始上翹。一個「2000 RPS 撐在 100 毫秒」的單點結果毫無定位價值——它可能離膝點還很遠、也可能已經在懸崖邊，單點測不出來。用哪個工具做這種階梯加壓、怎麼讀每一級的輸出，見 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法&lt;/a>；這裡要確立的是拐點判讀本身：拐點是一條曲線上的位置，要用曲線去找，不是一個點。&lt;/p>
&lt;h2 id="找出哪個資源先飽和">找出哪個資源先飽和&lt;/h2>
&lt;p>系統的容量由最先飽和的那個資源決定，所以每次 ramp-up 要同時盯多個維度，看哪個先到頂：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>CPU&lt;/strong>：看 load average 與 run queue，不只看利用率——利用率 100% 但 run queue 是空的仍撐得住，run queue 開始堆才是真的 CPU 飽和。Docker 跑在虛擬機裡或與其他工作負載共用主機時，虛擬機裡的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-average/" data-link-title="Load Average（負載平均）" data-link-desc="看到 uptime 或 node_load1 的數字、要判斷主機是不是在排隊時，查 load average 算的是什麼、除以核心數怎麼讀、Linux 為什麼把等磁碟的行程也算進去，以及在虛擬機與容器裡量到的值代表誰">load average&lt;/a> 看不到主機上的排隊（見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26&lt;/a>〈監控所在層看不到的範圍〉），加壓的每一級要附上當時主機本身的 load（見 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/shared-host-contention/" data-link-title="共用主機的資源競爭：容量估算、故障範圍、隔離層級與監控的位置" data-link-desc="多個服務、資料庫與監控放在同一台主機（或同一台實體機上的多台虛擬機）時，資源競爭在容量估算、故障範圍、隔離的做法與監控的位置上各自要考慮什麼：共用發生在哪些層、量到的數字什麼時候能互相比較、哪些服務適合放在一起、從同機加限制到專用實體機的隔離層級與各層的觸發條件">共用主機的資源競爭&lt;/a>〈容量估算〉）。&lt;/li>
&lt;li>&lt;strong>記憶體&lt;/strong>：垃圾回收暫停、swap、快取驅逐都是隱性的記憶體飽和，不會反映在單純的用量數字上。&lt;/li>
&lt;li>&lt;strong>磁碟 I/O&lt;/strong>：吞吐量、IOPS、佇列深度三個維度分開看——雲端 SSD 通常先撞 IOPS 上限，本機 NVMe 先撞吞吐量上限。&lt;/li>
&lt;li>&lt;strong>網路&lt;/strong>：頻寬、每秒封包數（PPS）、連線數。雲端的 PPS 上限超過時可能是靜默丟包，不報錯。&lt;/li>
&lt;li>&lt;strong>連線池&lt;/strong>：最常見的隱性瓶頸。一個大小 100 的連線池用到 95 就已經接近飽和，但 CPU、記憶體都還很閒——瓶頸不在機器、在池子。&lt;/li>
&lt;li>&lt;strong>外部 API 配額&lt;/strong>：看對方回的 429 錯誤率，這個上限不在自己手上。&lt;/li>
&lt;li>&lt;strong>檔案描述元與連接埠&lt;/strong>：ephemeral port 與 file descriptor 是 OS 層的配額，跟應用的連線池是不同層——連線池還沒滿、但 OS 的 fd 或可用 port 先耗盡，一樣接不了新連線，且症狀（&lt;code>Too many open files&lt;/code>、connect 失敗）容易被誤判成應用問題。&lt;/li>
&lt;/ul>
&lt;p>這幾種配額型飽和（連線池、外部 API 配額、fd 與 port）共用一個歸因陷阱：報錯的元件與佔住配額的元件通常不是同一個，因為佔用量是申請頻率乘上持有時間，而症狀只落在申請最頻繁的那個。確認飽和之後要接的動作是查系統保存的持有者紀錄（連線池的 checkout owner、&lt;code>lsof&lt;/code> 的持有 PID、配額系統的 principal），判讀順序與它的兩個失效前提見 &lt;a href="https://tarrragon.github.io/blog/report/resource-exhaustion-symptom-vs-holder/" data-link-title="配額耗盡的症狀落在申請最頻繁的元件、成因在持有最久的那個" data-link-desc="共享配額耗盡型故障要歸因時使用。症狀的位置由申請頻率決定，佔用量由申請頻率乘持有時間決定，而持有時間主導後者；系統保存的持有者紀錄能把搜尋範圍縮到一個元件">配額耗盡的症狀落在申請最頻繁的元件、成因在持有最久的那個&lt;/a>。&lt;/p>
&lt;p>還有一種隱性飽和不在這六維裡：熱分區（hot partition）。分散式的鍵值或資料庫，名義容量是每個分區上限乘以分區數，但鍵分布不均時，整體利用率可能只有 20%、最熱的那個分區已經 100%，再加流量立刻被限流。訊號是「吞吐量上不去、但平均利用率很低」加上「某些鍵的延遲飆升、出現限流事件」。處理靠複合鍵、寫入分片或用快取吸收熱鍵——這種飽和用整體利用率完全看不到。&lt;/p>
&lt;h2 id="該擴容之後往垂直還是水平">該擴容之後：往垂直還是水平&lt;/h2>
&lt;p>確認要擴之後，下一個判斷是往哪個軸擴。垂直擴展（換更大的機器）不必改程式，但受單機物理上限限制、成本非線性（高階機型有溢價）、且是單點；水平擴展（加更多台）理論上線性，但要求服務無狀態或狀態能同步，且每台都要付基礎成本。經驗法則是無狀態的部分水平擴、有狀態的關鍵節點（如資料庫主節點）垂直擴加讀取副本，兩者常同時進行。&lt;/p>
&lt;p>垂直擴展有兩道牆要知道。第一道是物理上限，但真正的天花板常常比規格更早到——雲端有記憶體達 TiB 級的超大機型，但有狀態的交易型資料庫主節點，真實天花板常卡在 32 到 64 vCPU，因為鎖競爭、context switch、記憶體頻寬這些架構因素，不是規格不夠。第二道是成本曲線：中階機型從 4 到 8 vCPU、8 到 16 vCPU 大約是 1.8 到 1.9 倍（接近線性），但過了 48 vCPU 明顯偏離線性、高階機型溢價更陡。垂直擴到一個點之後，每多一分效能付的錢急遽變貴，那就是該轉水平或該分片的訊號。&lt;/p></description><content:encoded><![CDATA[<p>什麼訊號代表該擴容、什麼代表可以縮容，規模拐點判斷的根本概念是飽和曲線：系統不是「撐得住」跟「掛掉」的二元，中間有一段可測量的劣化區。負載往上加，系統先在延遲平穩的線性區，過了某個點進入延遲開始上升的膝點區（knee），再往上就是延遲不可預測的懸崖區（cliff）。拐點判斷的核心，是在系統進入膝點區、還沒到懸崖時就讀出訊號，而不是等它掉下懸崖。</p>
<p>判斷標準先建立在利用率的三段區間上。利用率低於 50% 是線性區，延遲平穩；50% 到 80% 是膝點區，延遲開始上升但仍可接受；超過 80% 是懸崖區，延遲不可預測、可能逾時或引發級聯故障。健康的系統應該運轉在 50% 到 70% 之間，這也是 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a> 裡安全係數要覆蓋的那段 headroom。</p>
<p>這三段區間背後是排隊理論：利用率往高處走，佇列長度增加的幅度越來越陡——50% 到 70% 增加數倍、70% 到 90% 再急遽放大（實際倍數視佇列模型而定，但方向一致是超線性）；延遲跟佇列長度成正比，所以延遲在高利用率區是指數成長。80% 這個懸崖門檻是通用的形狀，膝點的實際位置隨系統類型變——無狀態服務的膝點大約在 80% CPU、有鎖競爭的資料庫大約 60%、吃磁碟 I/O 的佇列服務可能 50% 就到。這就是為什麼安全係數不能對所有系統套同一個數：有狀態的關鍵系統膝點來得早，要留的 headroom 比無狀態服務厚。判斷一個系統該在多少利用率就準備擴容，要看它是哪一類、膝點在哪，不是一律套 80%。</p>
<h2 id="膝點的早期訊號p99-先於平均劣化">膝點的早期訊號：p99 先於平均劣化</h2>
<p>膝點的價值在於它是「該擴容」的早期訊號，但它只在對的指標上看得見。四個訊號共同指向系統正在進入膝點區：吞吐量從線性成長轉為次線性（加壓上去、吞吐不再等比增加）、p50 還穩但 p99 與 p999 開始飆、佇列開始堆積（不只是利用率上升）、而錯誤率仍接近零。最後這條很關鍵——膝點區的系統還沒開始報錯，只看錯誤率會以為一切正常，要看尾延遲才抓得到。</p>
<p>尾延遲的敏感度是這裡的判讀核心。p50 對垃圾回收暫停、重試風暴、長尾都不敏感，純看平均或 p50 會誤判「飽和還沒到」；p99 對連線競爭敏感，能較早看到飽和的苗頭；p999 對垃圾回收的 stop-the-world、leader 選舉、重試風暴最敏感。看容量該不該擴，看的是 p99 跟 p999 的曲線有沒有開始上翹，不是平均值。等到吞吐量開始下降、p99 變成逾時、錯誤率飆升、重試風暴出現，那已經是掉進懸崖區了，擴容是在救火而不是預防。懸崖區裡服務呈現哪一種結果，由先耗盡的資源與等待是否設有上限決定，見 <a href="/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現</a>。</p>
<h2 id="用-ramp-up-找膝點不用單點壓測">用 ramp-up 找膝點，不用單點壓測</h2>
<p>膝點的位置要靠階梯加壓（ramp-up）找，不能用固定的單一壓力值測。按 1 倍、2 倍、4 倍、8 倍逐級加壓，每一級維持 5 到 10 分鐘看穩態行為，才能看出曲線在哪個負載開始上翹。一個「2000 RPS 撐在 100 毫秒」的單點結果毫無定位價值——它可能離膝點還很遠、也可能已經在懸崖邊，單點測不出來。用哪個工具做這種階梯加壓、怎麼讀每一級的輸出，見 <a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a>；這裡要確立的是拐點判讀本身：拐點是一條曲線上的位置，要用曲線去找，不是一個點。</p>
<h2 id="找出哪個資源先飽和">找出哪個資源先飽和</h2>
<p>系統的容量由最先飽和的那個資源決定，所以每次 ramp-up 要同時盯多個維度，看哪個先到頂：</p>
<ul>
<li><strong>CPU</strong>：看 load average 與 run queue，不只看利用率——利用率 100% 但 run queue 是空的仍撐得住，run queue 開始堆才是真的 CPU 飽和。Docker 跑在虛擬機裡或與其他工作負載共用主機時，虛擬機裡的 <a href="/blog/backend/knowledge-cards/load-average/" data-link-title="Load Average（負載平均）" data-link-desc="看到 uptime 或 node_load1 的數字、要判斷主機是不是在排隊時，查 load average 算的是什麼、除以核心數怎麼讀、Linux 為什麼把等磁碟的行程也算進去，以及在虛擬機與容器裡量到的值代表誰">load average</a> 看不到主機上的排隊（見 <a href="/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26</a>〈監控所在層看不到的範圍〉），加壓的每一級要附上當時主機本身的 load（見 <a href="/blog/devops/operations/05-capacity-planning/shared-host-contention/" data-link-title="共用主機的資源競爭：容量估算、故障範圍、隔離層級與監控的位置" data-link-desc="多個服務、資料庫與監控放在同一台主機（或同一台實體機上的多台虛擬機）時，資源競爭在容量估算、故障範圍、隔離的做法與監控的位置上各自要考慮什麼：共用發生在哪些層、量到的數字什麼時候能互相比較、哪些服務適合放在一起、從同機加限制到專用實體機的隔離層級與各層的觸發條件">共用主機的資源競爭</a>〈容量估算〉）。</li>
<li><strong>記憶體</strong>：垃圾回收暫停、swap、快取驅逐都是隱性的記憶體飽和，不會反映在單純的用量數字上。</li>
<li><strong>磁碟 I/O</strong>：吞吐量、IOPS、佇列深度三個維度分開看——雲端 SSD 通常先撞 IOPS 上限，本機 NVMe 先撞吞吐量上限。</li>
<li><strong>網路</strong>：頻寬、每秒封包數（PPS）、連線數。雲端的 PPS 上限超過時可能是靜默丟包，不報錯。</li>
<li><strong>連線池</strong>：最常見的隱性瓶頸。一個大小 100 的連線池用到 95 就已經接近飽和，但 CPU、記憶體都還很閒——瓶頸不在機器、在池子。</li>
<li><strong>外部 API 配額</strong>：看對方回的 429 錯誤率，這個上限不在自己手上。</li>
<li><strong>檔案描述元與連接埠</strong>：ephemeral port 與 file descriptor 是 OS 層的配額，跟應用的連線池是不同層——連線池還沒滿、但 OS 的 fd 或可用 port 先耗盡，一樣接不了新連線，且症狀（<code>Too many open files</code>、connect 失敗）容易被誤判成應用問題。</li>
</ul>
<p>這幾種配額型飽和（連線池、外部 API 配額、fd 與 port）共用一個歸因陷阱：報錯的元件與佔住配額的元件通常不是同一個，因為佔用量是申請頻率乘上持有時間，而症狀只落在申請最頻繁的那個。確認飽和之後要接的動作是查系統保存的持有者紀錄（連線池的 checkout owner、<code>lsof</code> 的持有 PID、配額系統的 principal），判讀順序與它的兩個失效前提見 <a href="/blog/report/resource-exhaustion-symptom-vs-holder/" data-link-title="配額耗盡的症狀落在申請最頻繁的元件、成因在持有最久的那個" data-link-desc="共享配額耗盡型故障要歸因時使用。症狀的位置由申請頻率決定，佔用量由申請頻率乘持有時間決定，而持有時間主導後者；系統保存的持有者紀錄能把搜尋範圍縮到一個元件">配額耗盡的症狀落在申請最頻繁的元件、成因在持有最久的那個</a>。</p>
<p>還有一種隱性飽和不在這六維裡：熱分區（hot partition）。分散式的鍵值或資料庫，名義容量是每個分區上限乘以分區數，但鍵分布不均時，整體利用率可能只有 20%、最熱的那個分區已經 100%，再加流量立刻被限流。訊號是「吞吐量上不去、但平均利用率很低」加上「某些鍵的延遲飆升、出現限流事件」。處理靠複合鍵、寫入分片或用快取吸收熱鍵——這種飽和用整體利用率完全看不到。</p>
<h2 id="該擴容之後往垂直還是水平">該擴容之後：往垂直還是水平</h2>
<p>確認要擴之後，下一個判斷是往哪個軸擴。垂直擴展（換更大的機器）不必改程式，但受單機物理上限限制、成本非線性（高階機型有溢價）、且是單點；水平擴展（加更多台）理論上線性，但要求服務無狀態或狀態能同步，且每台都要付基礎成本。經驗法則是無狀態的部分水平擴、有狀態的關鍵節點（如資料庫主節點）垂直擴加讀取副本，兩者常同時進行。</p>
<p>垂直擴展有兩道牆要知道。第一道是物理上限，但真正的天花板常常比規格更早到——雲端有記憶體達 TiB 級的超大機型，但有狀態的交易型資料庫主節點，真實天花板常卡在 32 到 64 vCPU，因為鎖競爭、context switch、記憶體頻寬這些架構因素，不是規格不夠。第二道是成本曲線：中階機型從 4 到 8 vCPU、8 到 16 vCPU 大約是 1.8 到 1.9 倍（接近線性），但過了 48 vCPU 明顯偏離線性、高階機型溢價更陡。垂直擴到一個點之後，每多一分效能付的錢急遽變貴，那就是該轉水平或該分片的訊號。</p>
<p>水平擴展的隱性成本則在協調與連線放大。加到很多台後會引入協調成本（共識、分散式鎖帶來新的故障模式與延遲），以及連線池放大——100 台每台開 10 條連線，資料庫就要扛 1000 條連線，機器數線性成長、資料庫連線數也跟著線性成長，最後瓶頸從應用層移到資料庫的連線上限。這條在 <a href="/blog/devops/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">模組二 水平擴展</a> 展開；關鍵是水平擴展把成本換到協調與下游連線上，不是免費的線性擴張。</p>
<h2 id="擴縮訊號的判讀">擴縮訊號的判讀</h2>
<p>擴縮的自動化要選對訊號、設對門檻。常見的擴容訊號各有適用與失準：CPU 通用但對 I/O bound 的服務失準、佇列深度快且直接反映積壓、p95 延遲是用戶體驗訊號但等延遲出現才擴可能來不及。設定順序是先選訊號、再設門檻、再加冷卻時間（cool-down）防止反覆抖動。</p>
<p>幾個擴縮後的訊號直接指出問題出在哪：加了機器但 QPS 沒提升，代表有狀態殘留（流量沒被新機器分攤）；加了機器但資料庫連線爆掉，是連線池放大；自動擴縮反覆擴了又縮，是冷卻時間太短或訊號在抖動；尖峰時新機器來不及起來，是冷啟動太慢或預測不夠提前。這些訊號讀對了，才知道該調的是訊號、門檻、冷卻時間，還是根本不該用自動擴縮（可預期的尖峰要用排程式或預測式擴容，不是等訊號觸發）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>膝點以下該留多少 headroom、安全係數怎麼定 → <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>用 ramp-up 壓測實際找出膝點 → <a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a></li>
<li>擴容到底值不值得——把容量翻成成本 → <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a></li>
<li>水平擴展的無狀態前提與連線放大 → <a href="/blog/devops/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">模組二 水平擴展</a></li>
</ul>
]]></content:encoded></item><item><title>成本模型</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/cost-model/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/cost-model/</guid><description>&lt;p>成本模型把容量規劃的輸出翻成錢：資源規格乘上使用時間乘上計費模式，等於帳單。三個乘數裡，計費模式的選擇是成本模型的核心決策——同樣的資源規格、同樣的使用時間，選錯計費模式，成本可以差好幾倍。這一章建立成本模型的基本詞彙與算法，它是 &lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing 與成本監控告警；預算門檻、配額與停損層級的推算在本模組，各家的建置篇目前只有 GCP，放在雲端業者模組">模組八 成本管理&lt;/a> 的直接輸入，那裡會把這些詞彙展開成完整的成本治理。&lt;/p>
&lt;p>計費模式有三種，取捨的軸是「彈性 vs 單位成本」。On-demand（按用量計費）彈性最高、不用承諾，但單位成本最貴；reserved（預留，承諾 1 到 3 年）單位成本降 30% 到 60%，代價是承諾期內用不滿就浪費；spot（用雲端的閒置容量）單位成本降 70% 到 90%，代價是隨時可能被收回中斷。三者是組合使用、不是三選一。&lt;/p>
&lt;h2 id="三種計費模式的最佳組合">三種計費模式的最佳組合&lt;/h2>
&lt;p>成本最佳的配置是按工作負載的性質分配三種計費模式：永遠用得到的基準負載用 reserved（吃它的長期折扣）、峰值與無法預測的爆量用 on-demand（吃它的彈性）、不在關鍵路徑上、可以被中斷的批次工作用 spot（吃它的深折扣）。這個組合讓每一份負載都用到最匹配它性質的計費模式——基準負載穩定所以能承諾、峰值不穩定所以要彈性、批次工作能容忍中斷所以能吃 spot。&lt;/p>
&lt;p>Spot 的中斷風險要靠分散來管。把 spot 池同時涵蓋多種機型、多個可用區（例如同時要 m5、m5a、m5n 三種相近機型、每個可用區都有），單一機型的池被收回時，其他機型還在，工作不會整批中斷。Spot 便宜但會被收回，分散是讓「被收回」不等於「工作失敗」的辦法。&lt;/p>
&lt;h2 id="單位請求成本讓成本可歸因">單位請求成本讓成本可歸因&lt;/h2>
&lt;p>成本模型要能算到單位請求成本，才能判斷哪裡貴、哪裡值得優化。最粗的算法是月帳單總額除以月總請求數，得到平均的單位請求成本——但平均值藏了太多資訊。有用的做法是分階段拆解：一個請求的成本分攤到應用運算、資料庫讀、資料庫寫、快取、網路流出、第三方 API，各階段有自己的單位成本，加起來才是這個請求真正花的錢。&lt;/p>
&lt;p>再往下要分端點算。不同端點的成本差幾個數量級——一個登入請求可能是萬分之一美元，一個結帳請求可能是千分之一，差 10 倍，因為結帳走更多階段、可能跨區域、要呼叫第三方支付。把所有請求當等成本來估容量與收費，會嚴重誤判哪個功能在燒錢。最後把單位成本對齊業務指標（每活躍用戶成本、每筆交易成本、每次推論成本），成本才跟商業決策接得上，而不只是一個雲端帳單數字。&lt;/p>
&lt;h2 id="right-sizing把規格調到剛好">Right-sizing：把規格調到剛好&lt;/h2>
&lt;p>Right-sizing 是定期檢查每個資源的規格是不是配得剛好。常見的浪費有三種：訂了太大的機型（CPU、記憶體長期只用 30%）、用了過時的機型世代（還在用舊代、沒升級到單位效能更好的新代）、以及 reserved 買太多用不滿。這三種浪費都不會自己冒出來報警，要靠定期 review 才抓得到——成本模型要持續對照實際用量調整，建一次並不夠。&lt;/p>
&lt;h2 id="過度與不足配置的經濟學">過度與不足配置的經濟學&lt;/h2>
&lt;p>配多配少各有代價，成本模型的判斷是找兩者的平衡點。過度配置的成本很好算——每個月多付的錢，直接看帳單。不足配置的成本難算，它是「故障機率乘以每次停機時間乘以每分鐘的營收損失」，要靠歷史事故率跟停機影響估。這兩個成本的平衡點就是經濟上最佳的 headroom。&lt;/p>
&lt;p>平衡點會隨工作負載的關鍵程度移動。金融、醫療、支付這類關鍵負載，不足配置的代價（一次停機的損失）極高，寧可過度配置 30% 到 50%；內部工具、分析、批次這類非關鍵負載，停機代價低，可以貼近最低配置跑。同一套成本模型，關鍵負載算出來要留厚 headroom、非關鍵負載算出來可以壓薄——差別不在保守或積極，在不足配置的代價不同。&lt;/p>
&lt;h2 id="autoscaler-的-max-是財務斷路器">Autoscaler 的 max 是財務斷路器&lt;/h2>
&lt;p>自動擴縮的三個參數（最小、最大、目標）同時是成本的控制點。最小值太低會冷啟動、太高會平日浪費；目標利用率太高會等進膝點才反應、太低會頻繁擴縮浪費；而最大值是財務上的斷路器——它設多少，就是這個服務跟著執行個體數計費那一部分的單月成本上限（出站流量這類跟著用量計費的部分不受它限制，拿到憑證直接建資源的人也不經過它，兩者的上限見 &lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/spend-planning-before-renting/" data-link-title="租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層" data-link-desc="租用雲端主機或把試用帳號升級成付費帳戶之前，費用失控時從觸發到停止之間的金額怎麼從配額與訊號延遲推出來，再用它推扣款的信用卡額度、配額、預算警示與觸發處置的門檻；Oracle Cloud、Google Cloud 與 AWS 的扣款時點、預先授權、扣款失敗的後果、擋下建立與停止用量的機制各自涵蓋什麼，以及停損停到哪一層的判斷">租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層&lt;/a>〈停止之前的最大超支：配額、訊號延遲與處置延遲〉）。最大值設成無限大是常見的成本事故：一次流量異常（可能是攻擊、可能是重試風暴）觸發無上限擴容，月底收到爆掉的帳單。最大值要當成「願意為這個服務付的成本上限」來設，而不是「怕限流所以設高一點」。&lt;/p>
&lt;h2 id="自建與託管的人力成本">自建與託管的人力成本&lt;/h2>
&lt;p>成本模型只算雲端帳單會漏掉一大塊：人力。自建一套資料庫、佇列、快取，需要 DBA 或 SRE 持續維護（打補丁、備份、故障轉移）；託管服務把這些交給供應商。兩者的人力成本可以差幾倍到一個數量級（常見的經驗區間是 3 到 10 倍，實際看自管元件的維運複雜度）——一個資深 DBA 的年薪不便宜，而工程師花在維運自建系統上的時間是機會成本，那些時間本可以做產品。算自建划不划算，只比雲端費用而忽略人力，會得出錯誤的結論。真實的成本決策裡，這塊人力差常常比機器費用還大——有團隊把自管叢集換成託管，省下的主要是叢集管理的人力與維運簡化，而不是機器錢。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;ul>
&lt;li>成本模型的輸入——容量怎麼算出來 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷&lt;/a>&lt;/li>
&lt;li>峰值預備的成本（為峰值留的 headroom 要付多少）→ &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>&lt;/li>
&lt;li>預知的活動要擴容時，規格換成費用區間、對齊付款時點、做成給決策者與會計的數字 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準&lt;/a>&lt;/li>
&lt;li>完整的雲端成本治理、計費模式的深入操作 → &lt;a href="https://tarrragon.github.io/blog/devops/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing 與成本監控告警；預算門檻、配額與停損層級的推算在本模組，各家的建置篇目前只有 GCP，放在雲端業者模組">模組八 成本管理&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>成本模型把容量規劃的輸出翻成錢：資源規格乘上使用時間乘上計費模式，等於帳單。三個乘數裡，計費模式的選擇是成本模型的核心決策——同樣的資源規格、同樣的使用時間，選錯計費模式，成本可以差好幾倍。這一章建立成本模型的基本詞彙與算法，它是 <a href="/blog/devops/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing 與成本監控告警；預算門檻、配額與停損層級的推算在本模組，各家的建置篇目前只有 GCP，放在雲端業者模組">模組八 成本管理</a> 的直接輸入，那裡會把這些詞彙展開成完整的成本治理。</p>
<p>計費模式有三種，取捨的軸是「彈性 vs 單位成本」。On-demand（按用量計費）彈性最高、不用承諾，但單位成本最貴；reserved（預留，承諾 1 到 3 年）單位成本降 30% 到 60%，代價是承諾期內用不滿就浪費；spot（用雲端的閒置容量）單位成本降 70% 到 90%，代價是隨時可能被收回中斷。三者是組合使用、不是三選一。</p>
<h2 id="三種計費模式的最佳組合">三種計費模式的最佳組合</h2>
<p>成本最佳的配置是按工作負載的性質分配三種計費模式：永遠用得到的基準負載用 reserved（吃它的長期折扣）、峰值與無法預測的爆量用 on-demand（吃它的彈性）、不在關鍵路徑上、可以被中斷的批次工作用 spot（吃它的深折扣）。這個組合讓每一份負載都用到最匹配它性質的計費模式——基準負載穩定所以能承諾、峰值不穩定所以要彈性、批次工作能容忍中斷所以能吃 spot。</p>
<p>Spot 的中斷風險要靠分散來管。把 spot 池同時涵蓋多種機型、多個可用區（例如同時要 m5、m5a、m5n 三種相近機型、每個可用區都有），單一機型的池被收回時，其他機型還在，工作不會整批中斷。Spot 便宜但會被收回，分散是讓「被收回」不等於「工作失敗」的辦法。</p>
<h2 id="單位請求成本讓成本可歸因">單位請求成本讓成本可歸因</h2>
<p>成本模型要能算到單位請求成本，才能判斷哪裡貴、哪裡值得優化。最粗的算法是月帳單總額除以月總請求數，得到平均的單位請求成本——但平均值藏了太多資訊。有用的做法是分階段拆解：一個請求的成本分攤到應用運算、資料庫讀、資料庫寫、快取、網路流出、第三方 API，各階段有自己的單位成本，加起來才是這個請求真正花的錢。</p>
<p>再往下要分端點算。不同端點的成本差幾個數量級——一個登入請求可能是萬分之一美元，一個結帳請求可能是千分之一，差 10 倍，因為結帳走更多階段、可能跨區域、要呼叫第三方支付。把所有請求當等成本來估容量與收費，會嚴重誤判哪個功能在燒錢。最後把單位成本對齊業務指標（每活躍用戶成本、每筆交易成本、每次推論成本），成本才跟商業決策接得上，而不只是一個雲端帳單數字。</p>
<h2 id="right-sizing把規格調到剛好">Right-sizing：把規格調到剛好</h2>
<p>Right-sizing 是定期檢查每個資源的規格是不是配得剛好。常見的浪費有三種：訂了太大的機型（CPU、記憶體長期只用 30%）、用了過時的機型世代（還在用舊代、沒升級到單位效能更好的新代）、以及 reserved 買太多用不滿。這三種浪費都不會自己冒出來報警，要靠定期 review 才抓得到——成本模型要持續對照實際用量調整，建一次並不夠。</p>
<h2 id="過度與不足配置的經濟學">過度與不足配置的經濟學</h2>
<p>配多配少各有代價，成本模型的判斷是找兩者的平衡點。過度配置的成本很好算——每個月多付的錢，直接看帳單。不足配置的成本難算，它是「故障機率乘以每次停機時間乘以每分鐘的營收損失」，要靠歷史事故率跟停機影響估。這兩個成本的平衡點就是經濟上最佳的 headroom。</p>
<p>平衡點會隨工作負載的關鍵程度移動。金融、醫療、支付這類關鍵負載，不足配置的代價（一次停機的損失）極高，寧可過度配置 30% 到 50%；內部工具、分析、批次這類非關鍵負載，停機代價低，可以貼近最低配置跑。同一套成本模型，關鍵負載算出來要留厚 headroom、非關鍵負載算出來可以壓薄——差別不在保守或積極，在不足配置的代價不同。</p>
<h2 id="autoscaler-的-max-是財務斷路器">Autoscaler 的 max 是財務斷路器</h2>
<p>自動擴縮的三個參數（最小、最大、目標）同時是成本的控制點。最小值太低會冷啟動、太高會平日浪費；目標利用率太高會等進膝點才反應、太低會頻繁擴縮浪費；而最大值是財務上的斷路器——它設多少，就是這個服務跟著執行個體數計費那一部分的單月成本上限（出站流量這類跟著用量計費的部分不受它限制，拿到憑證直接建資源的人也不經過它，兩者的上限見 <a href="/blog/devops/operations/08-cost-management/spend-planning-before-renting/" data-link-title="租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層" data-link-desc="租用雲端主機或把試用帳號升級成付費帳戶之前，費用失控時從觸發到停止之間的金額怎麼從配額與訊號延遲推出來，再用它推扣款的信用卡額度、配額、預算警示與觸發處置的門檻；Oracle Cloud、Google Cloud 與 AWS 的扣款時點、預先授權、扣款失敗的後果、擋下建立與停止用量的機制各自涵蓋什麼，以及停損停到哪一層的判斷">租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層</a>〈停止之前的最大超支：配額、訊號延遲與處置延遲〉）。最大值設成無限大是常見的成本事故：一次流量異常（可能是攻擊、可能是重試風暴）觸發無上限擴容，月底收到爆掉的帳單。最大值要當成「願意為這個服務付的成本上限」來設，而不是「怕限流所以設高一點」。</p>
<h2 id="自建與託管的人力成本">自建與託管的人力成本</h2>
<p>成本模型只算雲端帳單會漏掉一大塊：人力。自建一套資料庫、佇列、快取，需要 DBA 或 SRE 持續維護（打補丁、備份、故障轉移）；託管服務把這些交給供應商。兩者的人力成本可以差幾倍到一個數量級（常見的經驗區間是 3 到 10 倍，實際看自管元件的維運複雜度）——一個資深 DBA 的年薪不便宜，而工程師花在維運自建系統上的時間是機會成本，那些時間本可以做產品。算自建划不划算，只比雲端費用而忽略人力，會得出錯誤的結論。真實的成本決策裡，這塊人力差常常比機器費用還大——有團隊把自管叢集換成託管，省下的主要是叢集管理的人力與維運簡化，而不是機器錢。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>成本模型的輸入——容量怎麼算出來 → <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>峰值預備的成本（為峰值留的 headroom 要付多少）→ <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>預知的活動要擴容時，規格換成費用區間、對齊付款時點、做成給決策者與會計的數字 → <a href="/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/" data-link-title="預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準" data-link-desc="行銷活動、檔期這類事先知道的大流量，容量規劃怎麼從業務預估換算成峰值請求量、各層要開幾台與擴容的計費時數，再換算成費用區間；三點估計怎麼帶著預估的誤差，給決策者的選項要附規格、費用、不足時的後果與回退方式，給會計的預算要對齊增量與付款時點，以及每次活動之後校準換算係數、沒有歷史資料時怎麼起步">預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</a></li>
<li>完整的雲端成本治理、計費模式的深入操作 → <a href="/blog/devops/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing 與成本監控告警；預算門檻、配額與停損層級的推算在本模組，各家的建置篇目前只有 GCP，放在雲端業者模組">模組八 成本管理</a></li>
</ul>
]]></content:encoded></item><item><title>預知的大流量要加多大、編多少預算：從業務預估到擴容規格與費用區間的換算鏈、給決策者與會計的數字，以及每次活動之後的校準</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/</link><pubDate>Tue, 27 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/planned-peak-sizing-and-budget/</guid><description>&lt;p>這篇整理事先知道會有大流量（行銷活動、檔期、上架、直播）時，容量規劃的規格與預算怎麼從同一份業務預估推出來。各節依序是：決策者問「要加多大」與會計問「要多少預算」各自要的是什麼；從業務預估到峰值請求量、再到各層要開幾台的換算鏈，每一個換算係數從哪裡來、預估的誤差怎麼用三點估計帶著走；從台數到費用區間的算法，包括擴容的計費時數、計費模式、跟著用量計費的項目與付款時點；給決策者的選項要附哪些數字、各自適合什麼條件；以及每次活動之後怎麼校準換算係數、沒有歷史資料時怎麼起步。峰值的形狀與倍率階梯在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>，計費模式與單位請求成本在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型&lt;/a>，活動前的準備時程、預先擴容的做法與值班在 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備&lt;/a>，這裡不重寫。&lt;/p>
&lt;h2 id="決策者與會計各自要的數字規格與風險金額期間與付款時點">決策者與會計各自要的數字：規格與風險、金額期間與付款時點&lt;/h2>
&lt;p>事先知道的大流量，在活動之前通常會被問兩個問題，兩個問題要的數字不同：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>決策者問要加多大&lt;/strong>：要的是規格與風險，也就是加到多少台、多大的資料庫，以及不加或少加時會發生什麼（回應變慢、排隊、下單失敗的比例），才能在幾個選項之間選。&lt;/li>
&lt;li>&lt;strong>會計問要多少預算&lt;/strong>：要的是金額、期間與付款時點，也就是費用落在哪幾個月、哪一筆是一次性的、哪一筆是每月經常性的、什麼時候從信用卡或帳戶扣走，才能編進預算、確認付得出來。&lt;/li>
&lt;/ul>
&lt;p>規格與預算各估一次時，決策者核准的規格與會計核准的金額會對不上：活動前一週才發現預算買不到核准的規格，或預算編了、規格卻不是照那個預算規劃的。同一份業務預估推出來的兩份數字，改一個假設（例如預估人數加倍）時會一起變，也一起被看到，所以兩份數字從同一份預估推。預算先定下來的情形，同一條換算鏈倒過來算：從預算推出買得到的台數，再推出能服務的峰值，超出的部分交給下面〈給決策者的選項〉裡的降級或等候室。&lt;/p>
&lt;h2 id="從業務預估到台數換算鏈換算係數的來源與三點估計">從業務預估到台數：換算鏈、換算係數的來源與三點估計&lt;/h2>
&lt;p>業務預估是活動負責人給得出來的數字：預計多少人來、集中在哪個時段。台數是工程要準備的數字：每一層要開幾台。兩者之間是一條換算鏈，前三步各用一個換算係數乘或除，第四步把台數對到機型與規格，上一步的輸出是下一步的輸入：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>業務預估 → 峰值時段的使用者數&lt;/strong>：預計來的人數，乘上集中在峰值時段的比例（下文稱&lt;strong>集中比例&lt;/strong>）。預計人數要包含平常在同一時段就會來的訪客；不包含時，把平常同一時段的請求量加到第二步的結果上。集中比例依活動的形式差很多（限量搶購集中在開賣的頭幾分鐘、全館折扣分散在整天），要從同類活動的歷史量；峰值的形狀在 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>。&lt;/li>
&lt;li>&lt;strong>使用者數 → 到源站的峰值請求量&lt;/strong>：乘上每個使用者在峰值時段發出的請求數（下文稱&lt;strong>每人請求數&lt;/strong>），再乘上到源站的比例（1 減去 CDN 的命中率），再除以峰值時段的秒數，得到源站每秒要處理的請求數。每人請求數從平常的流量模型量（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立&lt;/a>）；活動期間的行為可能跟平常不同，例如延遲上升時使用者重新整理、客戶端重試，每人請求數會跟著變大。&lt;/li>
&lt;li>&lt;strong>峰值請求量 → 各層的台數&lt;/strong>：除以一台在目標利用率下的每秒處理量，結果無條件進位成整數台。一台的處理量從壓力測試量（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法&lt;/a>）；目標利用率指膝點處理量的比例（不是 CPU 使用率），設在膝點以下，留出的那段餘裕吸收秒級的波動與故障（&lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算&lt;/a>〈安全係數的數學根據在飽和曲線〉）。故障的單位常是一個可用區：部署在 N 個可用區、要承受掉一個時，剩下的主機會吃到原本 N ÷（N－1）倍的量，目標利用率要再壓低到這個比例以下。目標利用率每一層不同：資料庫這類有狀態、有鎖競爭的元件膝點來得早，它的目標利用率要取得比無狀態的應用伺服器低（各類元件留多少的討論在 &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>〈Headroom budget 設計〉，概念見 &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> 卡）。資料庫、快取、第三方 API 這些不一定能水平加台的層，要再乘上每個請求觸發幾次查詢或呼叫，並對照它們的上限；從延遲目標往下反推各層的方法在〈峰值估算〉的〈從業務指標反推各層峰值〉。&lt;/li>
&lt;li>&lt;strong>台數 → 規格&lt;/strong>：台數、機型、資料庫規格、頻寬。&lt;/li>
&lt;/ol>
&lt;p>預估本身的誤差，這篇從目標利用率拆出來另外處理。〈峰值估算〉與 9.6 把預估誤差也算進膝點以下的那段餘裕（9.6 的〈Headroom budget 設計〉把預測的誤差列為 headroom 要涵蓋的第一項）；這篇改用下面的三點估計把它帶著走，所以目標利用率留下的餘裕只涵蓋秒級的波動與故障，用 9.6 的比例時要先扣掉預測誤差那一份，不重複算兩次。&lt;/p>
&lt;p>換算鏈裡誤差最大的是集中比例與每人請求數：每台處理量是壓測量的，相對可靠；集中比例與每人請求數在沒辦過同類活動時是估的。這兩個係數各取三個值，保守（偏高）、預期、樂觀（偏低），高的配高的、低的配低的，整條鏈跑三次，得到峰值請求量的&lt;strong>三點估計&lt;/strong>（&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>示例（數字都是為了說明換算而設的）：預計 20 萬人來（已含平常同一時段的訪客），峰值時段是開賣的頭 10 分鐘（600 秒），不放 CDN、請求全部到源站。集中比例取 20%、30%、40%，每人請求數取 20、30、40。壓測量到一台應用伺服器在延遲開始上升時（膝點）約 420 請求/秒，目標利用率取 60%，約 252，取整成 250。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>估計&lt;/th>
 &lt;th>峰值請求量（每秒）&lt;/th>
 &lt;th>應用伺服器（每台 250 請求/秒）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>樂觀&lt;/td>
 &lt;td>200,000 × 0.2 × 20 ÷ 600 ≈ 1,333&lt;/td>
 &lt;td>6 台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>預期&lt;/td>
 &lt;td>200,000 × 0.3 × 30 ÷ 600 ＝ 3,000&lt;/td>
 &lt;td>12 台&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>保守&lt;/td>
 &lt;td>200,000 × 0.4 × 40 ÷ 600 ≈ 5,333&lt;/td>
 &lt;td>22 台&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>資料庫假設每個請求平均觸發 1 次查詢，壓測時 4,000 次/秒延遲開始上升，目標利用率取比應用層低的 50%，可以處理 2,000 次/秒，預期的 3,000 已經超過。實際要先從流量模型拆出讀與寫各佔多少：讀的部分用&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">讀取副本&lt;/a>或快取吸收，寫的部分要靠佇列非同步處理（&lt;a href="https://tarrragon.github.io/blog/devops/operations/07-burst-traffic/queue-buffering/" data-link-title="Queue 緩衝" data-link-desc="在 ingestion 和 processing 之間加 message queue 做 burst 緩衝 — Kafka / NATS / Redis Streams 的選型和引入條件">Queue 緩衝&lt;/a>）或更大的主資料庫。這次的瓶頸在資料庫，所以下面各個選項都要寫出資料庫那一層怎麼處理。&lt;/p></description><content:encoded><![CDATA[<p>這篇整理事先知道會有大流量（行銷活動、檔期、上架、直播）時，容量規劃的規格與預算怎麼從同一份業務預估推出來。各節依序是：決策者問「要加多大」與會計問「要多少預算」各自要的是什麼；從業務預估到峰值請求量、再到各層要開幾台的換算鏈，每一個換算係數從哪裡來、預估的誤差怎麼用三點估計帶著走；從台數到費用區間的算法，包括擴容的計費時數、計費模式、跟著用量計費的項目與付款時點；給決策者的選項要附哪些數字、各自適合什麼條件；以及每次活動之後怎麼校準換算係數、沒有歷史資料時怎麼起步。峰值的形狀與倍率階梯在 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>，計費模式與單位請求成本在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>，活動前的準備時程、預先擴容的做法與值班在 <a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備</a>，這裡不重寫。</p>
<h2 id="決策者與會計各自要的數字規格與風險金額期間與付款時點">決策者與會計各自要的數字：規格與風險、金額期間與付款時點</h2>
<p>事先知道的大流量，在活動之前通常會被問兩個問題，兩個問題要的數字不同：</p>
<ul>
<li><strong>決策者問要加多大</strong>：要的是規格與風險，也就是加到多少台、多大的資料庫，以及不加或少加時會發生什麼（回應變慢、排隊、下單失敗的比例），才能在幾個選項之間選。</li>
<li><strong>會計問要多少預算</strong>：要的是金額、期間與付款時點，也就是費用落在哪幾個月、哪一筆是一次性的、哪一筆是每月經常性的、什麼時候從信用卡或帳戶扣走，才能編進預算、確認付得出來。</li>
</ul>
<p>規格與預算各估一次時，決策者核准的規格與會計核准的金額會對不上：活動前一週才發現預算買不到核准的規格，或預算編了、規格卻不是照那個預算規劃的。同一份業務預估推出來的兩份數字，改一個假設（例如預估人數加倍）時會一起變，也一起被看到，所以兩份數字從同一份預估推。預算先定下來的情形，同一條換算鏈倒過來算：從預算推出買得到的台數，再推出能服務的峰值，超出的部分交給下面〈給決策者的選項〉裡的降級或等候室。</p>
<h2 id="從業務預估到台數換算鏈換算係數的來源與三點估計">從業務預估到台數：換算鏈、換算係數的來源與三點估計</h2>
<p>業務預估是活動負責人給得出來的數字：預計多少人來、集中在哪個時段。台數是工程要準備的數字：每一層要開幾台。兩者之間是一條換算鏈，前三步各用一個換算係數乘或除，第四步把台數對到機型與規格，上一步的輸出是下一步的輸入：</p>
<ol>
<li><strong>業務預估 → 峰值時段的使用者數</strong>：預計來的人數，乘上集中在峰值時段的比例（下文稱<strong>集中比例</strong>）。預計人數要包含平常在同一時段就會來的訪客；不包含時，把平常同一時段的請求量加到第二步的結果上。集中比例依活動的形式差很多（限量搶購集中在開賣的頭幾分鐘、全館折扣分散在整天），要從同類活動的歷史量；峰值的形狀在 <a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>。</li>
<li><strong>使用者數 → 到源站的峰值請求量</strong>：乘上每個使用者在峰值時段發出的請求數（下文稱<strong>每人請求數</strong>），再乘上到源站的比例（1 減去 CDN 的命中率），再除以峰值時段的秒數，得到源站每秒要處理的請求數。每人請求數從平常的流量模型量（<a href="/blog/devops/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a>）；活動期間的行為可能跟平常不同，例如延遲上升時使用者重新整理、客戶端重試，每人請求數會跟著變大。</li>
<li><strong>峰值請求量 → 各層的台數</strong>：除以一台在目標利用率下的每秒處理量，結果無條件進位成整數台。一台的處理量從壓力測試量（<a href="/blog/devops/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a>）；目標利用率指膝點處理量的比例（不是 CPU 使用率），設在膝點以下，留出的那段餘裕吸收秒級的波動與故障（<a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a>〈安全係數的數學根據在飽和曲線〉）。故障的單位常是一個可用區：部署在 N 個可用區、要承受掉一個時，剩下的主機會吃到原本 N ÷（N－1）倍的量，目標利用率要再壓低到這個比例以下。目標利用率每一層不同：資料庫這類有狀態、有鎖競爭的元件膝點來得早，它的目標利用率要取得比無狀態的應用伺服器低（各類元件留多少的討論在 <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>〈Headroom budget 設計〉，概念見 <a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a> 卡）。資料庫、快取、第三方 API 這些不一定能水平加台的層，要再乘上每個請求觸發幾次查詢或呼叫，並對照它們的上限；從延遲目標往下反推各層的方法在〈峰值估算〉的〈從業務指標反推各層峰值〉。</li>
<li><strong>台數 → 規格</strong>：台數、機型、資料庫規格、頻寬。</li>
</ol>
<p>預估本身的誤差，這篇從目標利用率拆出來另外處理。〈峰值估算〉與 9.6 把預估誤差也算進膝點以下的那段餘裕（9.6 的〈Headroom budget 設計〉把預測的誤差列為 headroom 要涵蓋的第一項）；這篇改用下面的三點估計把它帶著走，所以目標利用率留下的餘裕只涵蓋秒級的波動與故障，用 9.6 的比例時要先扣掉預測誤差那一份，不重複算兩次。</p>
<p>換算鏈裡誤差最大的是集中比例與每人請求數：每台處理量是壓測量的，相對可靠；集中比例與每人請求數在沒辦過同類活動時是估的。這兩個係數各取三個值，保守（偏高）、預期、樂觀（偏低），高的配高的、低的配低的，整條鏈跑三次，得到峰值請求量的<strong>三點估計</strong>（<a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">Peak Forecast</a> 卡寫的最壞、預期、最好三點）；活動負責人對預計人數也沒把握時，人數一樣取三個值配進去。保守那一點是每個係數同時取偏高值的組合，不是機率上的某個分位數：兩個係數彼此獨立時，同時偏高的機率比各自偏高低，保守點會偏寬；兩者正相關時（越集中的活動，使用者越會重新整理）就不會。搶購機器人、重試風暴與預料之外的爆紅會超過保守點，這要靠下面選項裡的回退方式處理。三點離預期要拉多寬，看同類活動的歷史：有幾次的歷史時，取歷史上實際值與預估值的偏差範圍；只有一兩次、或沒有歷史時，一次活動只是一個樣本，偏差範圍不可靠，要取得更寬（下面〈每次活動之後的校準，以及沒有歷史資料時的起步〉）。</p>
<p>示例（數字都是為了說明換算而設的）：預計 20 萬人來（已含平常同一時段的訪客），峰值時段是開賣的頭 10 分鐘（600 秒），不放 CDN、請求全部到源站。集中比例取 20%、30%、40%，每人請求數取 20、30、40。壓測量到一台應用伺服器在延遲開始上升時（膝點）約 420 請求/秒，目標利用率取 60%，約 252，取整成 250。</p>
<table>
  <thead>
      <tr>
          <th>估計</th>
          <th>峰值請求量（每秒）</th>
          <th>應用伺服器（每台 250 請求/秒）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>樂觀</td>
          <td>200,000 × 0.2 × 20 ÷ 600 ≈ 1,333</td>
          <td>6 台</td>
      </tr>
      <tr>
          <td>預期</td>
          <td>200,000 × 0.3 × 30 ÷ 600 ＝ 3,000</td>
          <td>12 台</td>
      </tr>
      <tr>
          <td>保守</td>
          <td>200,000 × 0.4 × 40 ÷ 600 ≈ 5,333</td>
          <td>22 台</td>
      </tr>
  </tbody>
</table>
<p>資料庫假設每個請求平均觸發 1 次查詢，壓測時 4,000 次/秒延遲開始上升，目標利用率取比應用層低的 50%，可以處理 2,000 次/秒，預期的 3,000 已經超過。實際要先從流量模型拆出讀與寫各佔多少：讀的部分用<a href="/blog/backend/knowledge-cards/read-write-split/" data-link-title="Read-Write Split" data-link-desc="說明讀寫流量如何分流到 primary 與 replica，以及它引入的一致性責任">讀取副本</a>或快取吸收，寫的部分要靠佇列非同步處理（<a href="/blog/devops/operations/07-burst-traffic/queue-buffering/" data-link-title="Queue 緩衝" data-link-desc="在 ingestion 和 processing 之間加 message queue 做 burst 緩衝 — Kafka / NATS / Redis Streams 的選型和引入條件">Queue 緩衝</a>）或更大的主資料庫。這次的瓶頸在資料庫，所以下面各個選項都要寫出資料庫那一層怎麼處理。</p>
<p>這條鏈的價值在每一步都看得到換算係數從哪裡來。決策者問「為什麼是 22 台」時，答案是一串可以逐項檢查的乘除，而且看得到保守那一點是哪幾個係數取了偏高的值。</p>
<h2 id="從台數到預算計費時數計費模式跟著用量計費的項目與付款時點">從台數到預算：計費時數、計費模式、跟著用量計費的項目與付款時點</h2>
<p>台數換成金額要決定幾件事：加開的資源計費多少小時、單價用哪一種計費模式，以及不跟著台數走的費用。給會計的是<strong>增量</strong>，也就是活動期間比平常多出來的費用，平常的費用已經在每月的預算裡：</p>
<ul>
<li><strong>計費時數</strong>：加開的資源從加開那一刻開始計費，到縮回為止。活動開始之前要提前多久加開（下文稱<strong>預先擴容</strong>，pre-scaling），有兩種起點：只依預熱（快取與連線先熱起來，快取的部分見 <a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">Cache Warmup</a>；<a href="/blog/backend/09-performance-capacity/peak-event-readiness/" data-link-title="9.11 高峰事件準備" data-link-desc="可預期峰值從 T-90 到 T-0 的準備時程：容量計畫、quota 申請、壓測、game day 與 pre-scaling 各排在事件前幾天、排在那裡的理由與項目之間的依賴，pre-scaling 的做法、watch room、event tier 分級與準備時間不夠時被壓縮的部分，以及事後 retro">9.11 高峰事件準備</a>〈Pre-scaling 策略〉寫的是 30 分鐘到 2 小時）與加開之後驗證規格的時間決定時，是活動前幾小時；照 9.11〈T-90 → T-0 準備時程〉在活動前兩天開始預先擴容時，計費時數從兩天前起算，換到的是更早發現開不出機器或規格不對、還有時間處理。配額申請、正式的壓測這些前置工作要提早好幾週排，它們不算在加開資源的計費時數裡。活動結束之後也不會當場縮回：要確認每秒請求數回到平常的水準、並維持一段時間；流量有日週期，觀察到隔天同一時段，才看得到是否有回流的使用者，所以示例取一個完整的日週期。增量費用＝加開的台數 × 單價 × 計費時數，加開的台數＝所需台數－平常的台數，所需台數不超過平常的台數時不加開。</li>
<li><strong>預先擴容還是交給自動擴縮</strong>：峰值爬升得比自動擴縮的反應時間（指標取樣、開機、通過健康檢查，通常要幾分鐘，推論，各家沒有逐一查）還快時，例如開賣頭幾分鐘就爆量，容量要預先加開到峰值，自動擴縮只能吸收活動中段的變化。預先加開可以手動調高台數，也可以用排程擴縮（<a href="/blog/backend/knowledge-cards/scheduled-scaling/" data-link-title="Scheduled Scaling" data-link-desc="說明按已知時間表預先擴容的 autoscaler 模式">Scheduled Scaling</a>）在指定的時間把自動擴縮的最小台數調高、活動結束後再調回，計費時數照排程的起訖時間算。預先加開時主機費用是固定的；交給自動擴縮時，主機費用是各時段的加開台數 × 單價 × 時數加總起來，上限是全程開滿保守點的台數，下限取決於擴縮的最小台數與預熱期間開了幾台。</li>
<li><strong>計費模式</strong>：活動只有幾天，加開的部分照隨用隨付（on-demand）計。折扣型的預留（reserved instance、savings plan、承諾使用折扣 committed use discount）是一年起跳的承諾，只適合平常就用得到的基準負載（<a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈三種計費模式的最佳組合〉；savings plan 這類承諾模式在 <a href="/blog/devops/operations/08-cost-management/billing-models/" data-link-title="計費模式理解" data-link-desc="看懂雲端帳單怎麼生成時，先認清收費的維度、on-demand/reserved/savings plan/spot 的承諾差異、以及 egress 這類最常被忽略的隱藏成本">計費模式理解</a>〈承諾模式：用彈性換折扣〉）；為了一次活動買它，活動之後用不滿的部分照樣每月付。可被收回的執行個體（spot）單價低，但峰值正是那種機型最搶手、最可能被收回的時候，被收回時正好補不回來，所以不用在活動的服務容量上；它適合用在活動前產生壓測流量。</li>
<li><strong>容量預留</strong>：它買的是「當天開得出機器」的保證，不是折扣，跟折扣型的預留是兩回事。預留期間不論有沒有用都照單價計費，所以它是計費時數的另一個起點：從建立預留那一刻算起，不是從加開那一刻。活動當天在那個可用區可能開不出機器時才需要它。例如 AWS 立即生效的容量預留（On-Demand Capacity Reservations）沒有承諾期、可以隨時取消，沒有折扣，算進隨用隨付執行個體的配額；未來日期生效的那一種有承諾期，取消可能收費（<a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-capacity-reservations.html">Reserve compute capacity with EC2 On-Demand Capacity Reservations</a>，2026-10-27 讀）。各家預留容量的欄位名稱見 <a href="/blog/devops/infra/cloud-providers/vm-options/shape-and-image/" data-link-title="機型與映像檔：CPU 架構、核心與記憶體、計費模式與作業系統" data-link-desc="雲端主機的機型決定的 CPU 架構、核心與記憶體，計費模式（隨用隨付、可被收回、預留容量、專屬主機）各自的條件，映像檔與機型之間的架構相容性，以及哪些項目建立後可以調整">機型與映像檔：CPU 架構、核心與記憶體、計費模式與作業系統</a>〈各家的欄位名稱：機型、可被收回、預留容量與映像檔〉。</li>
<li><strong>資料庫那一層</strong>：加讀取副本照同一條式子算（加開的副本數 × 單價 × 計費時數）。垂直升級主資料庫不一樣：有些業者改規格要停機，升級與降回都要排在可以停機的時段，降回常常排在活動之後很久，計費時數從升級算到降回；儲存空間加上去之後通常縮不回來（例如 Amazon RDS 的文件寫「You can&rsquo;t deallocate space」，<a href="https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/USER_PIOPS.ModifyingExisting.html">Increasing DB instance storage capacity</a>，2026-10-27 讀），要算成之後每月的經常性費用，給會計時跟一次性的費用分開列。</li>
<li><strong>跟著用量計費的項目</strong>：出站流量＝活動期間的總請求數 × 平均回應大小 × 每 GB 單價，放 CDN 時分成 CDN 回應的那一段（照 CDN 的計價）與回源的那一段（依業者可能另外計費，推論，各家沒有逐一查）。它跟主機費用誰大取決於回應大小：平均回應到 MB 級（圖片、影片多的頁面）時，出站流量可能比主機費用還高。按請求計價的託管服務與 serverless（函式、API 閘道、按請求計費的資料庫）沒有台數，費用＝請求數 × 單價，三點估計直接影響的是金額；它們另有自己的爬升限制與帳號配額，要先查。另外還有按次計費的第三方 API（簡訊、金流、地圖）、負載平衡器依流量與連線數的計費、跨可用區的流量，以及私有子網路經由 NAT 閘道呼叫外部服務的處理費（這三項依業者，推論，單價沒有查），還有記錄與監控的資料量（流量變大時 log 也跟著變大）。</li>
<li><strong>活動本身的準備費用</strong>：壓測本身也要開資源與產生流量，這筆費用在活動之前就發生。值班與待命的人力＝人數 × 時數 × 費率，時數含預先擴容與活動之後的觀察期；有夜間或假日值班時，這一項常常比雲端費用高。</li>
</ul>
<p>示例（接換算鏈示例的數字，單價、平均回應大小與預熱驗證的時數都是為了說明而設的）：平常開 4 台應用伺服器，每台每小時 0.2 美元；提前 6 小時加開（預熱 2 小時加上驗證 4 小時）、活動 24 小時、觀察 24 小時，計費時數 54 小時；出站流量每 GB 0.08 美元，平均回應 50 KB。</p>
<table>
  <thead>
      <tr>
          <th>估計</th>
          <th>加開台數</th>
          <th>主機增量（54 小時）</th>
          <th>出站流量（當天總量）</th>
          <th>出站流量費用（當天總量）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>樂觀</td>
          <td>2</td>
          <td>2 × 0.2 × 54 ≈ 22 美元</td>
          <td>20 萬人 × 20 × 50 KB ＝ 200 GB</td>
          <td>16 美元</td>
      </tr>
      <tr>
          <td>預期</td>
          <td>8</td>
          <td>8 × 0.2 × 54 ≈ 86 美元</td>
          <td>20 萬人 × 30 × 50 KB ＝ 300 GB</td>
          <td>24 美元</td>
      </tr>
      <tr>
          <td>保守</td>
          <td>18</td>
          <td>18 × 0.2 × 54 ≈ 194 美元</td>
          <td>20 萬人 × 40 × 50 KB ＝ 400 GB</td>
          <td>32 美元</td>
      </tr>
  </tbody>
</table>
<p>出站流量那兩欄用的是全天每人請求數：示例借用峰值時段的 20、30、40，等於假設全天的 20 萬人每人都發出那麼多請求（不論是不是在峰值時段來的）；全天每人請求數本身是另一個要量的係數。這兩欄是活動當天的總量，給會計的增量要扣掉平常一天的出站流量。平均回應大小從存取紀錄（access log）的回應位元組數或 CDN 的報表量；出站流量的單價與每月的免費量查業者價目表的出站（data transfer out）那一項。改成在活動前兩天開始預先擴容時，計費時數變成 48 ＋ 24 ＋ 24 ＝ 96 小時，比示例多 42 小時，保守那一列的主機增量變成 18 × 0.2 × 96 ≈ 346 美元。</p>
<p>給會計的預算是選定選項（下一節）之後的數字：主機費用依選定的做法取固定值（預先加開）或區間（交給自動擴縮），加上資料庫那一層、跟著用量走的項目的區間，並寫出區間從哪幾個係數來。預算編在哪一點有兩種做法：編在保守那一點，活動中不必再核准，代價是編多了的部分要在活動之後退回或留用；編在預期那一點，並寫明超過時由誰核准追加，代價是超出時要有人在活動中當天核准。追加預算的流程跟不上活動時，編在預期那一點的代價是峰值超出時沒有人能及時核准，服務只能停在預算內的規格，或在沒有核准的狀態下超支。</p>
<p>金額還要對齊三件事。一是付款時點：隨用隨付的帳戶多半是月結後付，下個月初扣上個月的費用，預算的月份與扣款的月份差一個月，跨月的活動分在兩期扣；Google Cloud 的門檻扣款帳戶例外，活動中費用暴增、餘額達到付款門檻時就會扣款。二是幣別與稅：雲端多半以美元計價，預算以本地幣別編，扣款金額還會受匯率、稅金與信用卡海外交易手續費影響，給會計的數字要寫出匯率的假設與是否含稅。三是臨時加開資源對停損機制的影響：加開資源要先調高配額，最大花費速度跟著變大，停止之前的最大超支、觸發處置的門檻與信用卡額度都要依新的值重算，否則活動當天正常的流量就會觸發停損（花費超過門檻時，由程式或人停止資源）。這三件事的推算與各家的出處在 <a href="/blog/devops/operations/08-cost-management/spend-planning-before-renting/" data-link-title="租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層" data-link-desc="租用雲端主機或把試用帳號升級成付費帳戶之前，費用失控時從觸發到停止之間的金額怎麼從配額與訊號延遲推出來，再用它推扣款的信用卡額度、配額、預算警示與觸發處置的門檻；Oracle Cloud、Google Cloud 與 AWS 的扣款時點、預先授權、扣款失敗的後果、擋下建立與停止用量的機制各自涵蓋什麼，以及停損停到哪一層的判斷">租用雲端主機之前的費用規劃：費用訊號的延遲、停止之前的最大超支、扣款的信用卡額度、配額與預算門檻的計算，以及停損停到哪一層</a>。</p>
<h2 id="給決策者的選項規格費用區間不足時的後果回退方式與適合的條件">給決策者的選項：規格、費用區間、不足時的後果、回退方式與適合的條件</h2>
<p>決策者要的是選擇，所以給的是幾個選項，每個選項附同一組數字：規格（含瓶頸那一層）、費用區間、容量不足時會發生什麼、活動中發現不夠時能怎麼補，以及這個選項適合什麼條件。「不足時的後果」分兩個門檻寫成可以觀察的結果：峰值超過目標利用率下的處理量時，秒級的尖峰開始讓延遲超過目標；超過膝點時，延遲與逾時明顯上升，逾時的比例用壓測量。以上面的示例（各選項的資料庫處置費用記為 &lt;資料庫處置的增量&gt;，出站流量 16 到 32 美元的當天總量各選項都一樣）：</p>
<ul>
<li><strong>依保守那一點預先加開（應用伺服器 22 台，主機增量約 194 美元，加上資料庫處理到每秒 5,333 次查詢的費用）</strong>：兩個門檻是每秒 5,500 與約 9,200（22 × 420），峰值落在保守點以內時不必降級。預估準確時多出的容量沒用到，多付的是保守與預期之間的那一段。前提是配額已經核准到 22 台，資料庫那一層也在活動前處理好。適合下單失敗的營收損失與商譽代價遠高於多付的雲端費用時。</li>
<li><strong>依預期那一點加開，超出的流量用降級處理（12 台，約 86 美元，加上資料庫處理到每秒 3,000 次的費用）</strong>：兩個門檻是每秒 3,000 與約 5,040（12 × 420）。峰值超過 3,000 時，關掉推薦、延後寄信，把核心交易維持在延遲目標內；觸發點設在目標利用率對應的處理量、不設在膝點，是讓降級在延遲開始上升之前就生效，目標利用率到膝點之間的那段餘裕除了吸收秒級的波動與故障，也留給降級開關生效的時間。峰值到保守點的 5,333 時，降級至少要卸掉約 6% 的請求才不會超過膝點，要卸掉約 44% 才回到觸發點；降級能卸掉多少，要在活動前量。雲端費用較低，但降級本身也有成本：用到被關掉功能的使用者看不到推薦、晚收到信，流失的轉換與客訴要算進去（<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>〈降級的成本邊界〉）。降級要先決定犧牲哪些功能、保住哪些（請求依優先等級卸除的做法在 <a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>〈負載卸除與限流的分工、請求的優先等級〉），並在活動前演練過（9.11 的〈Game day 演練〉）：沒有事先定好順序的系統，超載時所有功能會一起逾時或失敗。適合核心交易與周邊功能能拆開、降級演練過的時候。</li>
<li><strong>不加，超過容量的使用者進等候室（維持 4 台，主機增量 0，加上等候室的費用）</strong>：兩個門檻是每秒 1,000 與約 1,680（4 × 420），等候室依每秒 1,000 的容量分批放行（<a href="/blog/backend/09-performance-capacity/unplanned-traffic-surge/" data-link-title="9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路" data-link-desc="沒有準備窗口的瞬時大流量（突發流量、流量暴增）怎麼承接：依流量形狀分辨真人需求、應用層的濫用流量、體積型攻擊（DDoS），自動擴展的反應時間換算成緩衝，入口標記請求優先等級與負載卸除的順序，等候室與事前註冊這類准入控制，把體積型攻擊交給更大的邊緣網路，以及擴展跟不上時的退路">9.15 無預警瞬時大流量：流量來源辨識、擴展緩衝、請求優先等級、准入控制、體積型攻擊與退路</a>〈准入控制：虛擬等候室（virtual waiting room）、waitlist 與事前註冊〉）。每秒 1,000 在資料庫的 2,000 以內，資料庫那一層不必處理。等候室本身是一項服務：用現成的要付方案費（例如 Cloudflare 的 Waiting Room 只在 Business 與 Enterprise 方案提供，<a href="https://developers.cloudflare.com/waiting-room/plans/">Plans</a>，2026-10-27 讀），自建要開發與壓測的工時。適合流量的商業價值不高、活動可以分批開放，而且等候室事前已經建好、演練過的情形；代價是排隊的使用者有一部分會離開。</li>
<li><strong>改活動本身的形式，降低換算鏈的係數</strong>：事前登記、抽籤、分時段開賣可以降低集中比例，把落地頁做成 CDN 可快取的靜態頁可以降低到源站的比例。改的是業務端，費用多半比加機器低，代價是活動的形式要跟活動負責人談，有時會降低活動的效果。它可以跟前三個選項並用，先降係數再重跑一次換算鏈。</li>
</ul>
<p>「回退方式」寫活動中發現不夠時要多久補得上：自動擴縮還有多少空間、手動加台要多久、配額還能不能再調；配額申請被拒或來不及核准時，加開的台數停在已核准的配額，依保守那一點加開的選項到不了；已核准的台數低於預期點時，依預期加開也到不了，超出的部分交給降級（能卸掉多少要在活動前量）、等候室與改活動形式。也要寫活動取消或延期時哪些費用已經發生：預先擴容與容量預留從開始那一刻計費，壓測與預付的第三方點數（例如簡訊）多半拿不回來。</p>
<p>決策者比較的是多付的費用，對上不足時的後果發生的機率與代價。不足時的代價可以估成：失敗或放棄的請求數 × 轉換率 × 客單價，再加上客訴與流失的使用者；比法在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈過度與不足配置的經濟學〉。比期望值（代價乘上發生的機率）適合活動常辦、單次失敗可以承受的情形；比最壞情況適合一次失敗就會傷到商譽或違反合約、法規的情形。示例裡雲端費用只有數百美元，決策的金額多半落在人力與營收損失這兩項。</p>
<h2 id="每次活動之後的校準以及沒有歷史資料時的起步">每次活動之後的校準，以及沒有歷史資料時的起步</h2>
<p>換算鏈的準度來自校準：每次活動之後，把每一個換算係數的實際值記下來，跟事前的三點估計對照。〈峰值估算〉的〈用歷史倍率階梯推可預期峰值〉寫的是倍率與 headroom 的校準，9.11 的〈事後 retro〉寫的是活動後第七天回顧的時程；這裡加上的是換算鏈自己的係數：</p>
<ul>
<li><strong>集中比例</strong>：實際來了多少人、峰值時段佔了多少。</li>
<li><strong>每人請求數</strong>：峰值時段實際的請求量除以峰值時段實際的使用者數。系統飽和或有等候室限流的時段，伺服器收到的是被服務的量，不是需求量，要用需求端的訊號還原（CDN 邊緣的請求數、等候室的排隊人數、被拒絕或逾時的次數）；因為延遲上升而重試、重新整理的請求要分開記，下次估算時才分得出需求本身與系統變慢造成的放大。</li>
<li><strong>全天每人請求數</strong>：出站流量用的那一個，跟峰值時段的分開量。</li>
<li><strong>每台處理量</strong>：活動中各層的實際利用率與延遲，對照壓測的數字；差很多時，代表壓測的情境跟真實的使用行為不一樣，下次壓測的情境要改。</li>
<li><strong>單位請求成本</strong>：活動期間的實際增量費用除以請求量，拆成主機、資料庫、流量、第三方 API 各一項（單位請求成本的算法在 <a href="/blog/devops/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a>〈單位請求成本讓成本可歸因〉，費用怎麼歸因到各項在 <a href="/blog/devops/operations/08-cost-management/cost-monitoring/" data-link-title="成本監控與告警" data-link-desc="要讓雲端帳單不失控時，靠歸因把每筆花費追到擁有者、用異常告警抓突增、以及選 showback 或 chargeback 讓成本有人負責">成本監控與告警</a>〈歸因是監控的地基〉）。帳務明細落後數小時到兩天，這一項要等活動之後幾天、帳務資料補齊再算，跟 9.11 的第七天回顧可以排在一起。</li>
</ul>
<p>記下來的係數就是下一次活動換算鏈的輸入，也是平常評估「如果流量變成兩倍要多少錢」的依據。平常在單價或計費方式有變動時（業者調價、換機型世代、改用承諾方案），或至少在每次編下一期預算之前，用實際帳單重算一次單位請求成本；業務提出下一次活動的規模時，換算鏈直接拿最新的係數給出第一版的規格與預算。</p>
<p>沒有辦過同類活動、沒有歷史資料時，換算鏈裡最不確定的是集中比例與每人請求數。起步的做法有幾種，各自換到的依據與付出的代價不同：</p>
<ul>
<li><strong>拿相近的活動、公開案例或行銷端的數字當起點</strong>：換到一個有依據的初始值，代價是兩個活動的使用者行為不一定相同。拿來當起點的案例要先確認是同一類活動：限量搶購屬於開賣即爆、幾十分鐘結束的瞬間爆量，年度檔期屬於提前數月已知的可預期峰值，兩者的集中比例差很多（<a href="/blog/devops/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a> 開頭的五種峰值形狀）。行銷端的漏斗數字也可以當依據，例如推播的發送數乘上開啟率，推播送出之後的幾分鐘是集中最高的時段。</li>
<li><strong>先辦一次小規模的試辦</strong>（限量開賣、只對一部分使用者開放）：量到自己的集中比例與每人請求數，代價是要多辦一次、試辦本身的規模也要估。</li>
<li><strong>把保守點的係數取得更偏高（三點的區間跟著變寬），並確保能在活動中補容量</strong>：自動擴縮與配額留有空間，活動中有人盯著，代價是規格與預算的區間變大。</li>
<li><strong>壓測直接量「每台每秒能完成幾趟使用者旅程」</strong>：把瀏覽、加入購物車、結帳這一串請求當一個單位壓測，省掉每人請求數這個係數，代價是旅程的組成要先定義得夠接近真實。</li>
</ul>
<p>這幾種常常一起用：用相近的案例或行銷端的數字估初始值，用試辦校正，剩下的誤差交給三點估計的保守那一點與活動中的補容量。</p>
]]></content:encoded></item><item><title>容器化資源設計</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/container-resource-design/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/container-resource-design/</guid><description>&lt;p>容器的資源限制是容量規劃在容器化環境的落地：把「這個服務需要多少資源」這個規劃結果，轉成每個容器的 memory limit、CPU limit 與磁碟 I/O 控制，確保單一容器不會吃光 host 資源、拖垮同一台上的其他服務。這條限制設太緊會觸發 OOMKill 或 CPU throttle、設太鬆等於沒設，所以限制值要從服務的真實用量推出來，而不是拍板決定一個數字。&lt;/p>
&lt;h2 id="memory-限制從-baseline-推不從猜">Memory 限制：從 baseline 推，不從猜&lt;/h2>
&lt;p>Memory limit 要從觀察到的真實用量推，不是憑感覺配。做法是先讓服務在沒有硬限制的情況下跑至少 24 小時、涵蓋日常操作與定期 job（降採樣、清理），用 &lt;code>docker stats&lt;/code> 看容器的 &lt;code>MEM USAGE&lt;/code> 抓出 baseline。這個 baseline 不只是應用程式本身的 heap 與 stack，還要含 runtime 的開銷（Go 的 GC metadata、JVM 的 metaspace、Python 的 interpreter）、內嵌資料庫的 page cache、以及 HTTP server 的連線 buffer——這些加起來才是服務真正佔用的記憶體。&lt;/p>
&lt;p>有了 baseline，limit 常用的起點是峰值乘上一個安全係數：&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">Memory limit = baseline peak × 1.5&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>1.5 是經驗值，預留 burst 時的記憶體波動（大 batch 的 JSON 反序列化、查詢結果集暫存）。係數太大浪費資源、太小在 burst 時 OOMKill。OOMKill 的症狀很好認也很難查——容器突然消失、沒有 application log，因為它是被 kernel 的 OOM killer 直接砍掉、應用來不及寫任何東西。確認是不是 OOMKill 靠兩條指令：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">docker inspect &amp;lt;container&amp;gt; &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.[0].State.OOMKilled&amp;#39;&lt;/span> &lt;span class="c1"># true = 被 OOM killer 終止&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">dmesg &lt;span class="p">|&lt;/span> grep -i oom &lt;span class="c1"># kernel log 裡被殺的 process 與當時記憶體&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>確認之後的處理是提高 limit、或找出記憶體異常的根因（memory leak、unbounded cache、大結果集查詢）——單純調高 limit 只在 baseline 估太低時對，遇到 leak 只是把 OOMKill 延後。&lt;/p>
&lt;p>不同 runtime 的記憶體特性也要納入 limit 設計。Go 的 GC 自動管理、但預設不感知容器的 limit，要設 &lt;code>GOMEMLIMIT&lt;/code> 讓它在逼近上限時積極回收；JVM 的 &lt;code>-Xmx&lt;/code> 要設得小於容器 limit、留空間給 heap 之外的 native memory；Python 沒有 GC 上限、大 DataFrame 或大 dict 可能瞬間超限；Node.js 的 V8 heap 預設約 1.5GB、要用 &lt;code>--max-old-space-size&lt;/code> 配合容器 limit。共通的原則是 runtime 若不感知容器 limit，就會在 host 還有記憶體、但容器已達上限時被 OOMKill。&lt;/p>
&lt;h2 id="cpu-限制hard-limit-還是相對權重">CPU 限制：hard limit 還是相對權重&lt;/h2>
&lt;p>CPU 限制有兩種語意，對應不同的隔離需求。&lt;code>--cpus=0.5&lt;/code> 是 hard limit，最多用 0.5 個核心、超過就被 throttle，適合多容器共用一台主機、要嚴格隔離的場景；&lt;code>--cpu-shares=512&lt;/code> 是相對權重，跟其他容器按比例分 CPU、host 閒置時可以用更多，適合彈性分配。選哪種取決於「這個容器的 CPU 用量需不需要一個硬上限」。&lt;/p>
&lt;p>CPU throttle 跟 OOMKill 的差別是它不會 crash——症狀是延遲上升，請求處理時間從 10ms 變 100ms，因為容器的 CPU time 被 cgroup 暫停了（兩者在過載時的結果與監控訊號見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現&lt;/a>）。要確認有沒有被 throttle，看 cgroup 的統計：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">cat /sys/fs/cgroup/cpu/cpu.stat &lt;span class="c1"># nr_throttled 被限制次數、throttled_time 累計暫停奈秒&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I/O bound 的服務（例如主要時間花在磁碟寫入與 HTTP 收發的收集器）通常不需要嚴格的 CPU 限制，CPU 只在查詢處理（JSON 反序列化、聚合計算）時短暫用到。對這類服務設太緊的 hard limit，反而會在偶爾的計算尖峰上製造不必要的延遲。&lt;/p></description><content:encoded><![CDATA[<p>容器的資源限制是容量規劃在容器化環境的落地：把「這個服務需要多少資源」這個規劃結果，轉成每個容器的 memory limit、CPU limit 與磁碟 I/O 控制，確保單一容器不會吃光 host 資源、拖垮同一台上的其他服務。這條限制設太緊會觸發 OOMKill 或 CPU throttle、設太鬆等於沒設，所以限制值要從服務的真實用量推出來，而不是拍板決定一個數字。</p>
<h2 id="memory-限制從-baseline-推不從猜">Memory 限制：從 baseline 推，不從猜</h2>
<p>Memory limit 要從觀察到的真實用量推，不是憑感覺配。做法是先讓服務在沒有硬限制的情況下跑至少 24 小時、涵蓋日常操作與定期 job（降採樣、清理），用 <code>docker stats</code> 看容器的 <code>MEM USAGE</code> 抓出 baseline。這個 baseline 不只是應用程式本身的 heap 與 stack，還要含 runtime 的開銷（Go 的 GC metadata、JVM 的 metaspace、Python 的 interpreter）、內嵌資料庫的 page cache、以及 HTTP server 的連線 buffer——這些加起來才是服務真正佔用的記憶體。</p>
<p>有了 baseline，limit 常用的起點是峰值乘上一個安全係數：</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">Memory limit = baseline peak × 1.5</span></span></code></pre></div><p>1.5 是經驗值，預留 burst 時的記憶體波動（大 batch 的 JSON 反序列化、查詢結果集暫存）。係數太大浪費資源、太小在 burst 時 OOMKill。OOMKill 的症狀很好認也很難查——容器突然消失、沒有 application log，因為它是被 kernel 的 OOM killer 直接砍掉、應用來不及寫任何東西。確認是不是 OOMKill 靠兩條指令：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker inspect &lt;container&gt; <span class="p">|</span> jq <span class="s1">&#39;.[0].State.OOMKilled&#39;</span>   <span class="c1"># true = 被 OOM killer 終止</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">dmesg <span class="p">|</span> grep -i oom                                       <span class="c1"># kernel log 裡被殺的 process 與當時記憶體</span></span></span></code></pre></div><p>確認之後的處理是提高 limit、或找出記憶體異常的根因（memory leak、unbounded cache、大結果集查詢）——單純調高 limit 只在 baseline 估太低時對，遇到 leak 只是把 OOMKill 延後。</p>
<p>不同 runtime 的記憶體特性也要納入 limit 設計。Go 的 GC 自動管理、但預設不感知容器的 limit，要設 <code>GOMEMLIMIT</code> 讓它在逼近上限時積極回收；JVM 的 <code>-Xmx</code> 要設得小於容器 limit、留空間給 heap 之外的 native memory；Python 沒有 GC 上限、大 DataFrame 或大 dict 可能瞬間超限；Node.js 的 V8 heap 預設約 1.5GB、要用 <code>--max-old-space-size</code> 配合容器 limit。共通的原則是 runtime 若不感知容器 limit，就會在 host 還有記憶體、但容器已達上限時被 OOMKill。</p>
<h2 id="cpu-限制hard-limit-還是相對權重">CPU 限制：hard limit 還是相對權重</h2>
<p>CPU 限制有兩種語意，對應不同的隔離需求。<code>--cpus=0.5</code> 是 hard limit，最多用 0.5 個核心、超過就被 throttle，適合多容器共用一台主機、要嚴格隔離的場景；<code>--cpu-shares=512</code> 是相對權重，跟其他容器按比例分 CPU、host 閒置時可以用更多，適合彈性分配。選哪種取決於「這個容器的 CPU 用量需不需要一個硬上限」。</p>
<p>CPU throttle 跟 OOMKill 的差別是它不會 crash——症狀是延遲上升，請求處理時間從 10ms 變 100ms，因為容器的 CPU time 被 cgroup 暫停了（兩者在過載時的結果與監控訊號見 <a href="/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現</a>）。要確認有沒有被 throttle，看 cgroup 的統計：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">cat /sys/fs/cgroup/cpu/cpu.stat   <span class="c1"># nr_throttled 被限制次數、throttled_time 累計暫停奈秒</span></span></span></code></pre></div><p>I/O bound 的服務（例如主要時間花在磁碟寫入與 HTTP 收發的收集器）通常不需要嚴格的 CPU 限制，CPU 只在查詢處理（JSON 反序列化、聚合計算）時短暫用到。對這類服務設太緊的 hard limit，反而會在偶爾的計算尖峰上製造不必要的延遲。</p>
<h2 id="磁碟-iooverlay-的寫入放大">磁碟 I/O：overlay 的寫入放大</h2>
<p>容器的磁碟 I/O 有一個容易被忽略的成本：overlay filesystem 的寫入放大。Docker 的 overlay2 storage driver 把容器的寫入分層管理，每次寫入或修改檔案，overlay 在上層建立副本再改（copy-on-write）。對 SQLite 這類頻繁 fsync 的嵌入式資料庫，這層 copy-on-write 會增加 20% 到 40% 的寫入延遲——一個看起來只是「換個存放位置」的決定，實際上改變了資料庫的寫入效能。</p>
<p>需要高 I/O 效能的目錄，用 host volume 掛載繞過 overlay：<code>-v /host/path:/container/path</code> 讓寫入直接落到 host 檔案系統。適合走 volume 的是嵌入式資料庫的資料目錄（SQLite、BoltDB）、要持久化的 log、大量小檔案的 cache 目錄；不需要的是暫存檔（處理完就刪）與唯讀設定檔（overlay 讀取開銷小）。若連磁碟都不需要碰、只要記憶體中的暫存，用 tmpfs：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run --tmpfs /tmp:size<span class="o">=</span>64m ...</span></span></code></pre></div><p>tmpfs 適合不需持久化的高頻寫入（SDK 的離線 buffer、session 暫存）——寫在記憶體、完全不碰磁碟、也不吃 overlay 的放大。</p>
<h2 id="容器的健康檢查掛在探活模組">容器的健康檢查掛在探活模組</h2>
<p>容器層的 health check（Dockerfile 的 <code>HEALTHCHECK</code>、Compose 的 <code>healthcheck</code>）告訴 orchestrator 這個容器是否正常運作，並用一段啟動寬限期（<code>start_period</code>）避免服務還在初始化就被判死。health check 要探到多深在 <a href="/blog/devops/operations/04-service-health/health-check-endpoint/" data-link-title="Health check endpoint 設計" data-link-desc="設計服務的健康檢查端點時，判斷什麼回應算健康、check 要探到多深、依賴要不要一起檢查時回來讀">Health check endpoint 設計</a>；<code>HEALTHCHECK</code> 在 Compose、<code>docker run</code> 與 Swarm 下各自對應什麼行為、為什麼它沒有分開的 readiness 與 startup，在 <a href="/blog/devops/operations/04-service-health/container-healthcheck/" data-link-title="容器的健康檢查：Docker HEALTHCHECK 的狀態、unhealthy 之後由誰處理、depends_on 的啟動把關與 distroless 映像檔" data-link-desc="說明 Docker 的 HEALTHCHECK 與 Compose 的 healthcheck 怎麼運作：starting、healthy、unhealthy 三個狀態與時間參數的換算，容器變成 unhealthy 之後 Docker 與 restart policy 各自做什麼、不做什麼，depends_on 的 service_healthy 只在啟動時把關會帶來的連鎖失敗，以及沒有 shell 的 distroless 映像檔怎麼做健康檢查">容器的健康檢查</a>。容器資源設計只需知道健康檢查是容器生命週期的一部分、跟資源限制一起構成「容器跑得穩」的條件。要注意的是 Compose 與 <code>docker run</code> 下，容器變成 unhealthy 不會被重啟，restart policy 只在程序結束時動作；liveness 與 readiness 的語意分界見 <a href="/blog/devops/operations/04-service-health/liveness-vs-readiness/" data-link-title="Liveness 與 Readiness" data-link-desc="分不清該用哪種探針、或探針失敗後平台重啟了不該重啟的服務時，回來釐清 liveness、readiness、startup 三種探針各自宣告什麼、失敗後平台做什麼">模組四 Liveness 與 Readiness</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>資源限制值的上游——容量怎麼算出來 → <a href="/blog/devops/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>容器的健康檢查、probe 語意與自動重啟 → <a href="/blog/devops/operations/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">模組四 服務探活</a></li>
<li>單一容器的限制之外，整台主機上多個服務的資源競爭、故障範圍與隔離層級 → <a href="/blog/devops/operations/05-capacity-planning/shared-host-contention/" data-link-title="共用主機的資源競爭：容量估算、故障範圍、隔離層級與監控的位置" data-link-desc="多個服務、資料庫與監控放在同一台主機（或同一台實體機上的多台虛擬機）時，資源競爭在容量估算、故障範圍、隔離的做法與監控的位置上各自要考慮什麼：共用發生在哪些層、量到的數字什麼時候能互相比較、哪些服務適合放在一起、從同機加限制到專用實體機的隔離層級與各層的觸發條件">共用主機的資源競爭</a></li>
<li>監控 collector 的容器部署實例 → <a href="/blog/devops/monitoring/04-collector/container-deployment/" data-link-title="Container 部署設計" data-link-desc="Docker 部署 collector 的設計 — SQLite 在 overlay filesystem 的 I/O 考量、volume mount、graceful shutdown、資源限制">Container 部署設計</a></li>
</ul>
]]></content:encoded></item><item><title>共用主機的資源競爭：容量估算、故障範圍、隔離層級與監控的位置</title><link>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/shared-host-contention/</link><pubDate>Mon, 12 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/shared-host-contention/</guid><description>&lt;p>多個工作負載共用同一份硬體資源，是部署與容量規劃的一個設計條件；這篇整理它在容量估算、故障範圍、隔離與監控上各要考慮什麼。共用有三種常見的形態：一台主機上用 Docker Compose 跑整個系統（應用程式、資料庫、快取與監控都在一起）、雲端的虛擬機與其他租戶共用同一台實體機，以及開發用的筆電上 Docker 的虛擬機與其他程式共用 CPU 與磁碟。三者的共同點是：某一個工作負載用掉的資源，會從其他工作負載能用的那一份裡扣掉，而被扣掉的那一方沒有做錯任何事。這個現象叫 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/noisy-neighbor/" data-link-title="Noisy Neighbor（共用資源的鄰居干擾）" data-link-desc="服務沒有改動、自己的用量也不高卻整體變慢，或者同一台主機、同一台實體機上的其他工作負載一忙就跟著慢時，查共用資源的競爭從哪裡來、為什麼服務自己的監控看不到，以及隔離由淺到深有哪些做法">Noisy Neighbor&lt;/a>。本篇處理整台主機層級的取捨，單一容器的 CPU 與記憶體限制怎麼設另見 &lt;a href="https://tarrragon.github.io/blog/devops/operations/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="把容量規劃落到容器上時，怎麼定 memory 與 CPU 限制不觸發 OOMKill 或 throttle、以及 overlay filesystem 對高頻寫入的 I/O 影響">容器化資源設計&lt;/a>。&lt;/p>
&lt;h2 id="一次開發環境共用主機過載的量測">一次開發環境共用主機過載的量測&lt;/h2>
&lt;p>一個短網址服務的開發環境在同一台虛擬機裡跑入口 Nginx、兩個後端、PostgreSQL、Valkey 與監控，虛擬機由 macOS 上的 OrbStack 提供。Mac 上其他程式把 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/load-average/" data-link-title="Load Average（負載平均）" data-link-desc="看到 uptime 或 node_load1 的數字、要判斷主機是不是在排隊時，查 load average 算的是什麼、除以核心數怎麼讀、Linux 為什麼把等磁碟的行程也算進去，以及在虛擬機與容器裡量到的值代表誰">load average&lt;/a>（在跑或在等 CPU 的平均行程數，除以核心數大於 1 代表有行程在排隊）推到 55 到 78、持續數小時的那段時間，這台 8 顆核心的 Mac 上：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>所有工作都變慢&lt;/strong>：PostgreSQL 產生 200 萬個數字的查詢要 4 秒，同一個查詢在 Mac 沒被佔用時約一百多毫秒，相差二、三十倍；1.2 萬列的批次寫入要十幾秒。這個比較量的正是主機造成的差距。&lt;/li>
&lt;li>&lt;strong>各服務受影響的程度不同&lt;/strong>：外部探測要求回應時間低於 500 ms，Go 後端達標的比例是 92.5%、最慢 2.1 秒；Laravel 後端只有 49%、最慢 6.0 秒，未達標的全是慢、沒有錯誤（探測的回應時間也含探測程式自己的等待，見 4.27）。&lt;/li>
&lt;/ul>
&lt;p>同一段時間，監控自己讀取各 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/prometheus-exporter/" data-link-title="Prometheus Exporter（指標轉接程式）" data-link-desc="要監控一個不會自己輸出 Prometheus 指標的系統（主機、容器、資料庫、快取、Nginx）時，查 exporter 做什麼、它和應用程式自己開的 /metrics 差在哪，以及 exporter 讀得到資料卻缺標籤、或者讀不到它所在層以外的機器時怎麼發現">exporter&lt;/a> 的耗時也逼近逾時，而虛擬機裡的 load 除以核心數最高只有 1.28，從虛擬機的數字判斷不出 CPU 在排隊；這一面的量測與告警寫法見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/host-overload-alerting/" data-link-title="4.27 主機過載時的監控告警：可用與變慢分開判斷、依原因抑制、監控自身的等待" data-link-desc="多個服務共用一台主機而主機過載時，各服務的探測與告警會一起失敗：可用性與延遲分成兩個判斷的寫法、間歇的條件怎麼持續成立、以主機飽和抑制各服務變慢的規則與它的取捨、以合成告警驗證抑制、抑制藏起各服務受影響程度的差別、探測與 Prometheus 自己也在等 CPU 時的訊號，以及讓監控不比服務先失效的資源隔離；以一次開發環境的過載為例">4.27 主機過載時的監控告警&lt;/a>。容量拿不準、服務一起出事，是下面兩節的題目。&lt;/p>
&lt;h2 id="容量估算共用主機時分得到的資源量">容量估算：共用主機時分得到的資源量&lt;/h2>
&lt;p>容量估算把「每秒多少請求」換成「需要多少 CPU、記憶體與連線」，前提是這些資源在需要的時候拿得到。共用主機時，拿得到的量由其他工作負載決定：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>同一台主機上的其他服務&lt;/strong>：資料庫的重查詢、批次工作、監控本身的讀取，和線上的請求搶 CPU 與磁碟。估算時要把它們的尖峰時段疊在一起看，而不是各自算平均。&lt;/li>
&lt;li>&lt;strong>同一台實體機上的其他租戶&lt;/strong>：雲端的虛擬機分到的 CPU 時間會被虛擬化層（hypervisor）分給別的虛擬機，Linux 虛擬機裡的 steal 時間（&lt;code>/proc/stat&lt;/code> 的 steal，&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/prometheus-exporter/" data-link-title="Prometheus Exporter（指標轉接程式）" data-link-desc="要監控一個不會自己輸出 Prometheus 指標的系統（主機、容器、資料庫、快取、Nginx）時，查 exporter 做什麼、它和應用程式自己開的 /metrics 差在哪，以及 exporter 讀得到資料卻缺標籤、或者讀不到它所在層以外的機器時怎麼發現">node_exporter&lt;/a> 的 &lt;code>node_cpu_seconds_total{mode=&amp;quot;steal&amp;quot;}&lt;/code>）記錄這段被拿走的時間。&lt;/li>
&lt;li>&lt;strong>雲端機型自己的 CPU 額度&lt;/strong>：可突發型的機型（例如 AWS 的 T 系列）另有 CPU 額度：standard 模式下額度用完只剩基準效能；unlimited 模式（T3、T3a、T4g 等的預設）可以繼續突發，超出基準的部分另外計費。額度剩多少看雲端自己的監控（AWS 是 CloudWatch 的 &lt;code>CPUCreditBalance&lt;/code> 與 &lt;code>CPUSurplusCreditBalance&lt;/code>），虛擬機裡的 node_exporter 看不到它。&lt;/li>
&lt;li>&lt;strong>開發環境&lt;/strong>：筆電上的其他程式。這一層無法控制，所以在筆電上量到的數字只代表那一刻的那台機器。&lt;/li>
&lt;/ul>
&lt;p>所以量到的數字含有主機當下的負載：同一個服務在主機安靜時與過載時量到的上限，差的是主機而不是服務，拿來比較服務本身的表現（兩個版本、兩種設定）時兩者不能互相比較。量測紀錄因此附上當時主機的 load、核心數與可用磁碟，而且要在主機本身看，不是在 Docker 的虛擬機裡（macOS 用 &lt;code>uptime&lt;/code>、&lt;code>sysctl -n hw.ncpu&lt;/code>、&lt;code>df -h&lt;/code>，Linux 用 &lt;code>uptime&lt;/code>、&lt;code>nproc&lt;/code>、&lt;code>df -h&lt;/code>）；虛擬機裡的數字為什麼不能用，見 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26 單機容器服務的流量與資源監控&lt;/a>〈監控所在層看不到的範圍：虛擬機與主機〉。主機的 load 除以核心數或磁碟使用率超過事先定好的門檻時，量到的數字不拿去和其他時間比較服務的表現，採用的門檻也寫進紀錄。主機狀態要在量測期間持續記錄，不只在開始前看一次；壓測工具跑在同一台主機上時，它用的 CPU 也從服務那一份扣掉。單次取樣的數字本身也會跳動，判讀方式見 4.26〈單次取樣與時間窗〉。&lt;/p>
&lt;h2 id="故障範圍同一台主機上服務的相關失效">故障範圍：同一台主機上服務的相關失效&lt;/h2>
&lt;p>共用同一台主機的服務，失效是相關的：主機的 CPU、記憶體、磁碟或網路任一樣耗盡，上面全部的服務一起受影響；主機重開或磁碟寫滿，全部一起停。&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/failure-domain/" data-link-title="Failure Domain（失效域）" data-link-desc="判斷兩個元件會不會因為同一件事一起停掉、備援或監控是否獨立於被保護的對象時，查它們共用了哪些會失效的依賴">失效域&lt;/a>。實例是同一個開發環境稍早的一次：Mac 的磁碟剩不到 1 GB 時，OrbStack 的虛擬機連同裡面全部的容器被停掉了三次。&lt;/p></description><content:encoded><![CDATA[<p>多個工作負載共用同一份硬體資源，是部署與容量規劃的一個設計條件；這篇整理它在容量估算、故障範圍、隔離與監控上各要考慮什麼。共用有三種常見的形態：一台主機上用 Docker Compose 跑整個系統（應用程式、資料庫、快取與監控都在一起）、雲端的虛擬機與其他租戶共用同一台實體機，以及開發用的筆電上 Docker 的虛擬機與其他程式共用 CPU 與磁碟。三者的共同點是：某一個工作負載用掉的資源，會從其他工作負載能用的那一份裡扣掉，而被扣掉的那一方沒有做錯任何事。這個現象叫 <a href="/blog/backend/knowledge-cards/noisy-neighbor/" data-link-title="Noisy Neighbor（共用資源的鄰居干擾）" data-link-desc="服務沒有改動、自己的用量也不高卻整體變慢，或者同一台主機、同一台實體機上的其他工作負載一忙就跟著慢時，查共用資源的競爭從哪裡來、為什麼服務自己的監控看不到，以及隔離由淺到深有哪些做法">Noisy Neighbor</a>。本篇處理整台主機層級的取捨，單一容器的 CPU 與記憶體限制怎麼設另見 <a href="/blog/devops/operations/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="把容量規劃落到容器上時，怎麼定 memory 與 CPU 限制不觸發 OOMKill 或 throttle、以及 overlay filesystem 對高頻寫入的 I/O 影響">容器化資源設計</a>。</p>
<h2 id="一次開發環境共用主機過載的量測">一次開發環境共用主機過載的量測</h2>
<p>一個短網址服務的開發環境在同一台虛擬機裡跑入口 Nginx、兩個後端、PostgreSQL、Valkey 與監控，虛擬機由 macOS 上的 OrbStack 提供。Mac 上其他程式把 <a href="/blog/backend/knowledge-cards/load-average/" data-link-title="Load Average（負載平均）" data-link-desc="看到 uptime 或 node_load1 的數字、要判斷主機是不是在排隊時，查 load average 算的是什麼、除以核心數怎麼讀、Linux 為什麼把等磁碟的行程也算進去，以及在虛擬機與容器裡量到的值代表誰">load average</a>（在跑或在等 CPU 的平均行程數，除以核心數大於 1 代表有行程在排隊）推到 55 到 78、持續數小時的那段時間，這台 8 顆核心的 Mac 上：</p>
<ul>
<li><strong>所有工作都變慢</strong>：PostgreSQL 產生 200 萬個數字的查詢要 4 秒，同一個查詢在 Mac 沒被佔用時約一百多毫秒，相差二、三十倍；1.2 萬列的批次寫入要十幾秒。這個比較量的正是主機造成的差距。</li>
<li><strong>各服務受影響的程度不同</strong>：外部探測要求回應時間低於 500 ms，Go 後端達標的比例是 92.5%、最慢 2.1 秒；Laravel 後端只有 49%、最慢 6.0 秒，未達標的全是慢、沒有錯誤（探測的回應時間也含探測程式自己的等待，見 4.27）。</li>
</ul>
<p>同一段時間，監控自己讀取各 <a href="/blog/backend/knowledge-cards/prometheus-exporter/" data-link-title="Prometheus Exporter（指標轉接程式）" data-link-desc="要監控一個不會自己輸出 Prometheus 指標的系統（主機、容器、資料庫、快取、Nginx）時，查 exporter 做什麼、它和應用程式自己開的 /metrics 差在哪，以及 exporter 讀得到資料卻缺標籤、或者讀不到它所在層以外的機器時怎麼發現">exporter</a> 的耗時也逼近逾時，而虛擬機裡的 load 除以核心數最高只有 1.28，從虛擬機的數字判斷不出 CPU 在排隊；這一面的量測與告警寫法見 <a href="/blog/backend/04-observability/host-overload-alerting/" data-link-title="4.27 主機過載時的監控告警：可用與變慢分開判斷、依原因抑制、監控自身的等待" data-link-desc="多個服務共用一台主機而主機過載時，各服務的探測與告警會一起失敗：可用性與延遲分成兩個判斷的寫法、間歇的條件怎麼持續成立、以主機飽和抑制各服務變慢的規則與它的取捨、以合成告警驗證抑制、抑制藏起各服務受影響程度的差別、探測與 Prometheus 自己也在等 CPU 時的訊號，以及讓監控不比服務先失效的資源隔離；以一次開發環境的過載為例">4.27 主機過載時的監控告警</a>。容量拿不準、服務一起出事，是下面兩節的題目。</p>
<h2 id="容量估算共用主機時分得到的資源量">容量估算：共用主機時分得到的資源量</h2>
<p>容量估算把「每秒多少請求」換成「需要多少 CPU、記憶體與連線」，前提是這些資源在需要的時候拿得到。共用主機時，拿得到的量由其他工作負載決定：</p>
<ul>
<li><strong>同一台主機上的其他服務</strong>：資料庫的重查詢、批次工作、監控本身的讀取，和線上的請求搶 CPU 與磁碟。估算時要把它們的尖峰時段疊在一起看，而不是各自算平均。</li>
<li><strong>同一台實體機上的其他租戶</strong>：雲端的虛擬機分到的 CPU 時間會被虛擬化層（hypervisor）分給別的虛擬機，Linux 虛擬機裡的 steal 時間（<code>/proc/stat</code> 的 steal，<a href="/blog/backend/knowledge-cards/prometheus-exporter/" data-link-title="Prometheus Exporter（指標轉接程式）" data-link-desc="要監控一個不會自己輸出 Prometheus 指標的系統（主機、容器、資料庫、快取、Nginx）時，查 exporter 做什麼、它和應用程式自己開的 /metrics 差在哪，以及 exporter 讀得到資料卻缺標籤、或者讀不到它所在層以外的機器時怎麼發現">node_exporter</a> 的 <code>node_cpu_seconds_total{mode=&quot;steal&quot;}</code>）記錄這段被拿走的時間。</li>
<li><strong>雲端機型自己的 CPU 額度</strong>：可突發型的機型（例如 AWS 的 T 系列）另有 CPU 額度：standard 模式下額度用完只剩基準效能；unlimited 模式（T3、T3a、T4g 等的預設）可以繼續突發，超出基準的部分另外計費。額度剩多少看雲端自己的監控（AWS 是 CloudWatch 的 <code>CPUCreditBalance</code> 與 <code>CPUSurplusCreditBalance</code>），虛擬機裡的 node_exporter 看不到它。</li>
<li><strong>開發環境</strong>：筆電上的其他程式。這一層無法控制，所以在筆電上量到的數字只代表那一刻的那台機器。</li>
</ul>
<p>所以量到的數字含有主機當下的負載：同一個服務在主機安靜時與過載時量到的上限，差的是主機而不是服務，拿來比較服務本身的表現（兩個版本、兩種設定）時兩者不能互相比較。量測紀錄因此附上當時主機的 load、核心數與可用磁碟，而且要在主機本身看，不是在 Docker 的虛擬機裡（macOS 用 <code>uptime</code>、<code>sysctl -n hw.ncpu</code>、<code>df -h</code>，Linux 用 <code>uptime</code>、<code>nproc</code>、<code>df -h</code>）；虛擬機裡的數字為什麼不能用，見 <a href="/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26 單機容器服務的流量與資源監控</a>〈監控所在層看不到的範圍：虛擬機與主機〉。主機的 load 除以核心數或磁碟使用率超過事先定好的門檻時，量到的數字不拿去和其他時間比較服務的表現，採用的門檻也寫進紀錄。主機狀態要在量測期間持續記錄，不只在開始前看一次；壓測工具跑在同一台主機上時，它用的 CPU 也從服務那一份扣掉。單次取樣的數字本身也會跳動，判讀方式見 4.26〈單次取樣與時間窗〉。</p>
<h2 id="故障範圍同一台主機上服務的相關失效">故障範圍：同一台主機上服務的相關失效</h2>
<p>共用同一台主機的服務，失效是相關的：主機的 CPU、記憶體、磁碟或網路任一樣耗盡，上面全部的服務一起受影響；主機重開或磁碟寫滿，全部一起停。<a href="/blog/backend/knowledge-cards/blast-radius/" data-link-title="Blast Radius" data-link-desc="說明事故影響面如何估算與隔離">Blast Radius</a> 的估算因此要把「放在同一台主機」當成一條依賴，同一台主機上的服務落在同一個<a href="/blog/backend/knowledge-cards/failure-domain/" data-link-title="Failure Domain（失效域）" data-link-desc="判斷兩個元件會不會因為同一件事一起停掉、備援或監控是否獨立於被保護的對象時，查它們共用了哪些會失效的依賴">失效域</a>。實例是同一個開發環境稍早的一次：Mac 的磁碟剩不到 1 GB 時，OrbStack 的虛擬機連同裡面全部的容器被停掉了三次。</p>
<p>這帶出部署時要決定的幾件事：</p>
<ul>
<li><strong>哪些服務適合放在一起</strong>：延遲敏感的線上請求與吃滿 CPU 的批次工作（重算報表、匯出）放在一起時，批次工作的尖峰就是線上請求的延遲尖峰。分開放，或至少用限制與權重讓線上請求優先。</li>
<li><strong>資料庫與應用程式要不要同機</strong>：同機省下網路延遲與一台機器的成本，代價在記憶體。主機的記憶體耗盡時，Linux 的 <a href="/blog/linux/dotfile/knowledge-cards/oom-exit-code-137/" data-link-title="OOM Killer and Exit Code 137（OOM killer 與退出碼 137）" data-link-desc="程序或 container 被無預警砍掉、退出碼是 137、或編譯 / 測試在記憶體吃緊時突然死掉、要判斷是不是記憶體不足時回來讀">OOM killer</a> 依各行程常駐的記憶體（與可調的 <code>oom_score_adj</code>）挑一個結束，佔用最多的最先被挑中；資料常駐記憶體的 Redis、Valkey，或 <code>shared_buffers</code> 調大的 PostgreSQL，常是佔用最多的那一個（PostgreSQL 多數的快取在作業系統的 page cache，不算進去）。所以應用程式的記憶體暴增時，被結束的可能是資料庫或快取；PostgreSQL 的一個行程被結束，主行程會中斷所有連線、做損毀復原（crash recovery）之後才收得了新連線。給應用程式的容器設記憶體上限，暴增時被結束的就只會是那個容器；PostgreSQL 官方文件另建議把主行程的 <code>oom_score_adj</code> 調低。這時被終止的程序與負載增加的程序不是同一個，後端看到的是依賴失敗，見 <a href="/blog/backend/09-performance-capacity/overload-outcomes/" data-link-title="9.16 負載超過容量時服務的表現：變慢、請求失敗、無回應、程序終止，尖峰過去或應對切換之後仍持續的停在失效、擺盪與第二波湧入，以及應對選出的降級回應，各自的判斷標準、成因、應對與看得到它的監控層" data-link-desc="負載超過容量（過載）時，服務呈現的結果分成變慢、請求失敗（抵達後被拒與根本沒有抵達）、無回應、程序終止，以及尖峰過去、依賴恢復或應對切換之後仍持續的停在失效、擺盪與第二波湧入；每一種的判斷標準、通常的成因與應對，哪一種先出現由先耗盡的資源與等待是否設有上限決定，影響範圍怎麼隨時間擴大，服務端監控的各層各自看得到哪一種，降級回應怎麼計數，過載在資料上造成的重複寫入、接受後遺失與讀到舊資料，以及描述一次過載要寫出的項目">9.16 負載超過容量時服務的表現</a> 的〈程序終止：被系統結束、當機或反覆重啟〉。</li>
<li><strong>監控放在哪裡</strong>：監控與被監控的服務放在同一台主機時，主機出事的那一刻監控也跟著退化或停止，見〈監控的位置〉。</li>
</ul>
<h2 id="隔離的層級與代價">隔離的層級與代價</h2>
<p>隔離可以分成幾個層級，越往下隔離得越徹底，成本與管理負擔也越高：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>做法</th>
          <th>隔離得了</th>
          <th>隔離不了</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同一台主機，不設限制</td>
          <td>Compose 的預設</td>
          <td>—</td>
          <td>任何一個服務吃滿資源，其他全部受影響</td>
      </tr>
      <tr>
          <td>同一台主機，設上限與權重</td>
          <td>容器的 CPU 上限（<code>--cpus</code>，<a href="/blog/backend/knowledge-cards/cgroup/" data-link-title="cgroup（控制群組）" data-link-desc="設定容器的 CPU 與記憶體上限、讀 cAdvisor 的指標、或查容器為什麼被暫停與結束時，查 cgroup 是什麼、v1 與 v2 的差別，以及 Docker 與 Kubernetes 的參數各自對應到它的哪個控制檔">cgroup</a> 的 <code>cpu.max</code>）、把容器釘在指定核心（<code>--cpuset-cpus</code>）、相對權重（<code>--cpu-shares</code>，<code>cpu.weight</code>）、記憶體上限</td>
          <td>單一服務吃掉全部 CPU 或記憶體</td>
          <td>磁碟讀寫與網路的爭用、主機本身的故障</td>
      </tr>
      <tr>
          <td>同一台主機，保留資源與優先權</td>
          <td>Kubernetes 的 requests 與 QoS（requests 等於 limits 的 Pod 屬於 Guaranteed，最後才被 OOM 結束）、<code>PriorityClass</code>（資源不夠排程時先排它，必要時驅逐低優先權的 Pod）；cgroup v2 的記憶體保留（<code>memory.min</code>、<code>memory.low</code>，Docker 的 <code>--memory-reservation</code>）；Compose 的 <code>oom_score_adj</code>；systemd 的 slice（把一組服務放進同一個 cgroup，給它權重或上限）</td>
          <td>主機記憶體吃緊時，重要的服務與監控的記憶體最後被回收、最後被結束</td>
          <td>主機本身的故障</td>
      </tr>
      <tr>
          <td>不同主機或虛擬機</td>
          <td>依角色分開部署（資料庫、應用程式、監控各自一台），Kubernetes 的 anti-affinity（排程時不讓指定的 Pod 落在同一個節點）</td>
          <td>一台主機的資源耗盡與故障</td>
          <td>同一台實體機上其他租戶的競爭（虛擬機時）</td>
      </tr>
      <tr>
          <td>專用的實體機</td>
          <td>雲端的 dedicated host、自己的機房</td>
          <td>其他租戶，含共用的 CPU 快取與記憶體頻寬</td>
          <td>同一個機櫃、同一個機房的共同故障</td>
      </tr>
  </tbody>
</table>
<p>機房與可用區層級的隔離見 <a href="/blog/devops/operations/06-high-availability/redundancy-patterns/" data-link-title="冗餘設計模式" data-link-desc="在 active-passive、active-active、multi-region 之間選冗餘模式時，用資料同步方式與 standby 是否服務流量來區分，並知道冗餘不等於備份">冗餘設計模式</a>。</p>
<p>CPU 的硬上限有代價：碰到上限的容器會被暫停執行（CPU throttling），延遲因此升高而主機的 CPU 不一定滿（<a href="/blog/backend/knowledge-cards/resource-limit/" data-link-title="Resource Limit" data-link-desc="說明服務可使用的 CPU、memory 與相關資源上限如何影響行為">Resource Limit</a>）。單一容器的上限怎麼定見 <a href="/blog/devops/operations/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="把容量規劃落到容器上時，怎麼定 memory 與 CPU 限制不觸發 OOMKill 或 throttle、以及 overlay filesystem 對高頻寫入的 I/O 影響">容器化資源設計</a>〈CPU 限制：hard limit 還是相對權重〉，不設上限只設權重的取捨見 <a href="/blog/backend/05-deployment-platform/container-runtime/" data-link-title="5.1 container 與 runtime" data-link-desc="整理 image、resource limit 與啟動行為">5.1 container 與 runtime</a>〈CPU request 與 limit 的設定策略〉；核心依 <code>cpu.weight</code> 分配比例只在搶資源時生效，不會讓服務被暫停。</p>
<p>保留資源與優先權那一層在 CPU 上用的是同一個權重機制，它多出來的是記憶體耗盡時誰先被結束。這一層的觸發條件是：有服務沒設上限（監控元件常是這樣），或上限加總再加上主機自己的行程超過主機的記憶體，每個服務都沒超過自己的上限，主機一樣會耗盡記憶體；這時決定被結束順序的就是 QoS、優先權與 <code>oom_score_adj</code>，而 <code>memory.min</code>、<code>memory.low</code> 影響的是核心回收記憶體時先避開誰，不影響 OOM killer 挑誰結束。Kubernetes 上 kubelet 在節點記憶體或磁碟吃緊時會先依 QoS 驅逐 Pod，驅逐發生在核心的 OOM 之前。主機有 swap 時，耗盡的樣子常是全面變慢（不斷換頁），而不是某個行程被結束。</p>
<p>CPU 與記憶體的限制是成熟的做法；磁碟讀寫與網路頻寬的隔離（cgroup v2 的 <code>io.max</code>、<code>io.weight</code>）實際效果依磁碟與排程器而定，本篇沒有實測。磁碟爭用的樣子是：備份或大量匯入與資料庫的寫入同時進行時，資料庫每次提交都要等資料寫到磁碟上，寫入延遲跟著另一個服務的寫入量起伏，而兩邊的 CPU 用量都不高。所以高頻寫入的服務（資料庫、事件的寫入）在同一台主機上最容易互相拖累，也最常是第一個被分出去的。</p>
<p>選哪一個層級，看兩件事：一起出事的代價（故障範圍內的服務對使用者有多重要），以及拆開的成本（多一台機器、多一份部署與監控）。練習或早期的服務放在一台主機上是合理的起點，前提是知道哪些事會一起發生，並在它們開始發生時看得到。在一台主機上，「開始發生」長的是前面幾節的樣子：批次工作的時段線上請求的 p95 跟著上升、資料庫被 OOM killer 結束、磁碟寫滿讓全部服務一起停。前兩種先在同一台主機上補限制、權重或保留。磁碟寫滿先找出是誰寫滿的（日誌、映像、備份或資料庫本身），擴充磁碟或加上清理：Docker 預設的 json-file 日誌不會輪替，在 compose 設 <code>logging.options.max-size</code> 與 <code>max-file</code>；寫入量大的服務放在自己的檔案系統或設配額，寫滿時只影響它自己；同一個服務的寫入反覆把磁碟寫滿、或它的磁碟讀寫反覆拖慢資料庫，才是把它分到另一台主機的時候。限制、權重與保留都補了之後同一種情形仍反覆出現，也是分出去的時候。雲端虛擬機的 steal 時間長期偏高、換成非突發型機型也沒有改善時，才輪到專用的實體機。</p>
<h2 id="監控的位置">監控的位置</h2>
<p>裝在虛擬機裡的監控看不到虛擬機以外的競爭從哪裡來：Mac 上的其他程式完全看不到；實體機上的其他租戶只留下 <a href="/blog/backend/knowledge-cards/cpu-steal-time/" data-link-title="CPU Steal Time（被虛擬化層拿走的 CPU 時間）" data-link-desc="雲端虛擬機的服務沒有改動、CPU 用量也不高卻變慢時，查 steal time 記的是什麼、怎麼從 node_exporter 算出比例、什麼環境量不到它，以及升高時的處置">steal 時間</a>這一個間接訊號（見〈容量估算〉）。兩種情形在服務的指標上都長成全部服務同時變慢，而它們各自的用量都不高。各層的監控能看到什麼、主機的磁碟怎麼一起涵蓋，見 <a href="/blog/backend/04-observability/host-traffic-resource-monitoring/" data-link-title="4.26 單機容器服務的流量與資源監控：各層的訊號、Prometheus 與 exporter、監控所在層的盲區" data-link-desc="健康檢查之外的流量與資源監控：入口、應用程式、容器、主機與資料庫等服務內部各層的訊號從哪裡收、用哪一個 exporter，Prometheus 加 exporter、單一 agent、開箱即用工具與臨時查看指令的取捨，單次取樣與時間窗的差別，監控裝在虛擬機裡時看不到主機的 CPU 負載、看到的磁碟要分清是哪一顆，以及每接上一個來源的驗收與偵測容器停止的寫法；cAdvisor 與 node_exporter 在 OrbStack 上實測過">4.26 單機容器服務的流量與資源監控</a>。</p>
<p>監控和服務放在同一台主機時還會一起退化：探測與 Prometheus 自己也在等 CPU，量到的延遲包含它們自己的等待，讀取逾時會在資料裡留下空洞。所以正式環境至少要有一個訊號來自那台主機之外（另一個失效域的機器上的探測、外部的 uptime 服務；位置怎麼選見 <a href="/blog/backend/04-observability/single-host-monitoring-outside-view/" data-link-title="4.28 單台主機的監控盲區與失效域以外的觀察者：主機停機、使用者路徑中斷與告警鏈中斷，心跳監看與外部探測各自回答的問題與位置，以及與內部探測並排的判讀" data-link-desc="服務與監控放在同一台主機時監控結構上看不到的故障：主機整台停掉、使用者到主機的路徑中斷、告警鏈停掉與通知送不出去；從失效域以外送出訊號的心跳監看與外部探測各自回答的問題與成立條件，觀察者由第三方服務、另一個失效域的主機（含兩台主機互相監看）或另一個業者的排程平台提供時的差別，以及心跳監看、外部探測與主機內部探測並排時的判讀">4.28 單台主機的監控盲區與失效域以外的觀察者</a>，多台主機怎麼分配見 <a href="/blog/devops/operations/06-high-availability/small-service-host-layout/" data-link-title="小型服務的兩到三台主機配置：第二台與第三台主機的用法、監控主機的位置與連線、兩台主機互相監看，以及雲端業者負責的範圍" data-link-desc="一台主機的服務擴充到兩到三台時，每一台放什麼：第二台給監控、給應用程式的副本或給資料庫各自換到什麼與前提，與第三方心跳監看服務的比較，監控主機放在另一個業者或地端時經 VPN 讀取、連線中斷的誤判，兩台主機互相監看的條件，第三台給資料庫或兩台應用主機搭配託管資料庫，備份的位置，以及加主機也換不到、由雲端業者或入口設定處理的故障與攻擊">小型服務的兩到三台主機配置</a>），這是 <a href="/blog/backend/04-observability/observability-shared-fate/" data-link-title="4.25 觀測共命運失效：工具退化時的訊號與人層設計" data-link-desc="觀測系統跟被觀測系統共享失效域、在最需要時一起退化；out-of-band 訊號、優雅降級、以及工具盲飛時客服與工程師怎麼還能應對">4.25 觀測共命運失效</a> 在共用主機上的形態；告警怎麼寫、怎麼依主機飽和收斂各服務的症狀，見 <a href="/blog/backend/04-observability/host-overload-alerting/" data-link-title="4.27 主機過載時的監控告警：可用與變慢分開判斷、依原因抑制、監控自身的等待" data-link-desc="多個服務共用一台主機而主機過載時，各服務的探測與告警會一起失敗：可用性與延遲分成兩個判斷的寫法、間歇的條件怎麼持續成立、以主機飽和抑制各服務變慢的規則與它的取捨、以合成告警驗證抑制、抑制藏起各服務受影響程度的差別、探測與 Prometheus 自己也在等 CPU 時的訊號，以及讓監控不比服務先失效的資源隔離；以一次開發環境的過載為例">4.27 主機過載時的監控告警</a>。</p>
<h2 id="延伸閱讀單一容器的限制資源分艙與單點盤點">延伸閱讀：單一容器的限制、資源分艙與單點盤點</h2>
<ul>
<li>單一容器的記憶體與 CPU 限制怎麼從基準量推出來、CPU 用完配額而被暫停（throttling）時的症狀，見 <a href="/blog/devops/operations/05-capacity-planning/container-resource-design/" data-link-title="容器化資源設計" data-link-desc="把容量規劃落到容器上時，怎麼定 memory 與 CPU 限制不觸發 OOMKill 或 throttle、以及 overlay filesystem 對高頻寫入的 I/O 影響">容器化資源設計</a>。</li>
<li>本篇的隔離停在主機與容器這一層；同一個服務裡把連線池、執行緒依用途分開，讓一個依賴的故障不讓其他請求一起失敗，是同一個想法在應用層的做法，見 <a href="/blog/backend/knowledge-cards/bulkhead/" data-link-title="Bulkhead" data-link-desc="說明 bulkhead 如何用資源分艙限制故障擴散">Bulkhead</a> 與 <a href="/blog/devops/operations/03-traffic-management/bulkhead/" data-link-title="Bulkhead 隔離" data-link-desc="不同工作負載的資源池隔離 — 一個功能過載不拖垮其他功能的隔艙設計">Bulkhead 隔離</a>。</li>
<li>單點盤點的方法（逐條列出依賴、問每一條失效時誰受影響），見 <a href="/blog/devops/operations/06-high-availability/spof-inventory/" data-link-title="單點故障盤點" data-link-desc="要系統性找出哪些元件掛了整個系統就跟著掛時，用 pre-mortem 反推失敗路徑、依賴 budget 算可用性上限、以及按 blast radius 排序缺口">單點故障盤點</a>；「放在同一台主機」可以當成其中一條依賴列進去。</li>
</ul>
]]></content:encoded></item></channel></rss>