<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>模組五：容量規劃與壓力測試 on Tarragon</title><link>https://tarrragon.github.io/blog/operations/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/operations/05-capacity-planning/index.xml" rel="self" type="application/rss+xml"/><item><title>流量模型建立</title><link>https://tarrragon.github.io/blog/operations/05-capacity-planning/traffic-model/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/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/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/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/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>用壓測工具驗證這個模型撐不撐得住 → <a href="/blog/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/operations/05-capacity-planning/peak-estimation/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/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 比例、確認跨區域與跨可用區的分布，讓容量計畫有時間落地。&lt;/p>
&lt;h2 id="事件型峰值按最壞情況預備">事件型峰值按最壞情況預備&lt;/h2>
&lt;p>事件型的不可預期峰值沒有精確的發生時間，只有「一旦發生會多猛」的量級。一個賽事的進球、一則突發新聞，可以讓流量在幾秒內衝到平均的 10 到 50 倍。這種峰值估算的目標是「來的時候有多大的量級」，並讓系統隨時能承受那個量級——因為它可能在賽事的任何一分鐘發生，估「什麼時候來」沒有意義。&lt;/p>
&lt;p>這類峰值的預備常常不能只靠事後擴容，因為爆發是秒級的、而擴容有冷啟動延遲。可行的做法是事前把容量預先拉到能接住那個量級，或搭配預測式的擴容（在賽事的高風險時段提前擴），這牽涉到擴縮的時機判斷，在 &lt;a href="https://tarrragon.github.io/blog/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/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/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/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/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/operations/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、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 比例、確認跨區域與跨可用區的分布，讓容量計畫有時間落地。</p>
<h2 id="事件型峰值按最壞情況預備">事件型峰值按最壞情況預備</h2>
<p>事件型的不可預期峰值沒有精確的發生時間，只有「一旦發生會多猛」的量級。一個賽事的進球、一則突發新聞，可以讓流量在幾秒內衝到平均的 10 到 50 倍。這種峰值估算的目標是「來的時候有多大的量級」，並讓系統隨時能承受那個量級——因為它可能在賽事的任何一分鐘發生，估「什麼時候來」沒有意義。</p>
<p>這類峰值的預備常常不能只靠事後擴容，因為爆發是秒級的、而擴容有冷啟動延遲。可行的做法是事前把容量預先拉到能接住那個量級，或搭配預測式的擴容（在賽事的高風險時段提前擴），這牽涉到擴縮的時機判斷，在 <a href="/blog/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/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/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a></li>
<li>安全係數背後的飽和曲線、膝點怎麼找 → <a href="/blog/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>用壓測驗證系統撐不撐得住估出來的峰值 → <a href="/blog/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a></li>
<li>突發流量的即時應對（估不到的爆量怎麼撐）→ <a href="/blog/operations/07-burst-traffic/" data-link-title="模組七：突發流量應對" data-link-desc="行銷活動或新聞曝光帶來 10x-100x 流量時怎麼撐 — 突發分類、降級策略、queue 緩衝、規模分級應對">模組七 突發流量</a></li>
</ul>
]]></content:encoded></item><item><title>壓力測試工具與方法</title><link>https://tarrragon.github.io/blog/operations/05-capacity-planning/load-testing-tools/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/05-capacity-planning/load-testing-tools/</guid><description>&lt;p>壓測要回答的問題是「這個服務在什麼負載下開始撐不住」，不是「跑一次看會不會掛」。有價值的壓測是階梯加壓、觀察系統行為在哪個點從穩定轉為劣化，把 &lt;a href="https://tarrragon.github.io/blog/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/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>壓測時 throttle 到自己的工具</strong>：測出的是工具的極限、不是被測系統的，訊號是壓測機自己的 CPU 先滿。</li>
<li><strong>只看平均延遲</strong>：尾延遲看不到，p99 的劣化被平均值蓋掉。</li>
<li><strong>壓測環境跟 production 硬體不一致</strong>：CPU 型號、網路、磁碟 IOPS 差很多，結果不可外推。</li>
<li><strong>跑了壓測卻沒驗證模型</strong>：沒拿結果跟 production metric 比對，不知道壓測用的流量模型貼不貼近真實。</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>壓測用的流量模型怎麼建、怎麼驗證 → <a href="/blog/operations/05-capacity-planning/traffic-model/" data-link-title="流量模型建立" data-link-desc="要把「這個服務會收到什麼樣的流量」量化成容量規劃的輸入時，釐清該量哪幾個維度、峰均比為什麼是最關鍵的單一指標、以及怎麼從 production log 抽出可信的模型">流量模型建立</a></li>
<li>用階梯加壓找飽和點、讀哪些訊號 → <a href="/blog/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/operations/05-capacity-planning/scaling-inflection-point/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/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/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;/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/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 飽和。&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>
&lt;p>水平擴展的隱性成本則在協調與連線放大。加到很多台後會引入協調成本（共識、分散式鎖帶來新的故障模式與延遲），以及連線池放大——100 台每台開 10 條連線，資料庫就要扛 1000 條連線，機器數線性成長、資料庫連線數也跟著線性成長，最後瓶頸從應用層移到資料庫的連線上限。這條在 &lt;a href="https://tarrragon.github.io/blog/operations/02-horizontal-scaling/" data-link-title="模組二：水平擴展" data-link-desc="一個實例不夠時怎麼加第二個 — stateless 設計、shared storage、session 處理的工程約束">模組二 水平擴展&lt;/a> 展開；關鍵是水平擴展把成本換到協調與下游連線上，不是免費的線性擴張。&lt;/p>
&lt;h2 id="擴縮訊號的判讀">擴縮訊號的判讀&lt;/h2>
&lt;p>擴縮的自動化要選對訊號、設對門檻。常見的擴容訊號各有適用與失準：CPU 通用但對 I/O bound 的服務失準、佇列深度快且直接反映積壓、p95 延遲是用戶體驗訊號但等延遲出現才擴可能來不及。設定順序是先選訊號、再設門檻、再加冷卻時間（cool-down）防止反覆抖動。&lt;/p></description><content:encoded><![CDATA[<p>什麼訊號代表該擴容、什麼代表可以縮容，規模拐點判斷的根本概念是飽和曲線：系統不是「撐得住」跟「掛掉」的二元，中間有一段可測量的劣化區。負載往上加，系統先在延遲平穩的線性區，過了某個點進入延遲開始上升的膝點區（knee），再往上就是延遲不可預測的懸崖區（cliff）。拐點判斷的核心，是在系統進入膝點區、還沒到懸崖時就讀出訊號，而不是等它掉下懸崖。</p>
<p>判準先建立在利用率的三段區間上。利用率低於 50% 是線性區，延遲平穩；50% 到 80% 是膝點區，延遲開始上升但仍可接受；超過 80% 是懸崖區，延遲不可預測、可能逾時或引發級聯故障。健康的系統應該運轉在 50% 到 70% 之間，這也是 <a href="/blog/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 變成逾時、錯誤率飆升、重試風暴出現，那已經是掉進懸崖區了，擴容是在救火而不是預防。</p>
<h2 id="用-ramp-up-找膝點不用單點壓測">用 ramp-up 找膝點，不用單點壓測</h2>
<p>膝點的位置要靠階梯加壓（ramp-up）找，不能用固定的單一壓力值測。按 1 倍、2 倍、4 倍、8 倍逐級加壓，每一級維持 5 到 10 分鐘看穩態行為，才能看出曲線在哪個負載開始上翹。一個「2000 RPS 撐在 100 毫秒」的單點結果毫無定位價值——它可能離膝點還很遠、也可能已經在懸崖邊，單點測不出來。用哪個工具做這種階梯加壓、怎麼讀每一級的輸出，見 <a href="/blog/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 飽和。</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/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/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>用 ramp-up 壓測實際找出膝點 → <a href="/blog/operations/05-capacity-planning/load-testing-tools/" data-link-title="壓力測試工具與方法" data-link-desc="要用壓測驗證容量規劃時，釐清壓測到底在找什麼、k6 與 wrk 各自的定位、怎麼讀壓測輸出的延遲分布，以及讓結果失真的幾個反模式">壓力測試工具與方法</a></li>
<li>擴容到底值不值得——把容量翻成成本 → <a href="/blog/operations/05-capacity-planning/cost-model/" data-link-title="成本模型" data-link-desc="把容量規劃的資源翻成錢時，釐清 on-demand、reserved、spot 三種計費模式的取捨、怎麼算單位請求成本、以及過度與不足配置各自的經濟代價">成本模型</a></li>
<li>水平擴展的無狀態前提與連線放大 → <a href="/blog/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/operations/05-capacity-planning/cost-model/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/operations/05-capacity-planning/cost-model/</guid><description>&lt;p>成本模型把容量規劃的輸出翻成錢：資源規格乘上使用時間乘上計費模式，等於帳單。三個乘數裡，計費模式的選擇是成本模型的核心決策——同樣的資源規格、同樣的使用時間，選錯計費模式，成本可以差好幾倍。這一章建立成本模型的基本詞彙與算法，它是 &lt;a href="https://tarrragon.github.io/blog/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">模組八 成本管理&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;/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/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/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/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">模組八 成本管理&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>成本模型把容量規劃的輸出翻成錢：資源規格乘上使用時間乘上計費模式，等於帳單。三個乘數裡，計費模式的選擇是成本模型的核心決策——同樣的資源規格、同樣的使用時間，選錯計費模式，成本可以差好幾倍。這一章建立成本模型的基本詞彙與算法，它是 <a href="/blog/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">模組八 成本管理</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>自動擴縮的三個參數（最小、最大、目標）同時是成本的控制點。最小值太低會冷啟動、太高會平日浪費；目標利用率太高會等進膝點才反應、太低會頻繁擴縮浪費；而最大值是財務上的斷路器——它設多少，就是這個服務單月成本的上限。最大值設成無限大是常見的成本事故：一次流量異常（可能是攻擊、可能是重試風暴）觸發無上限擴容，月底收到爆掉的帳單。最大值要當成「願意為這個服務付的成本上限」來設，而不是「怕限流所以設高一點」。</p>
<h2 id="自建與託管的人力成本">自建與託管的人力成本</h2>
<p>成本模型只算雲端帳單會漏掉一大塊：人力。自建一套資料庫、佇列、快取，需要 DBA 或 SRE 持續維護（打補丁、備份、故障轉移）；託管服務把這些交給供應商。兩者的人力成本可以差幾倍到一個數量級（常見的經驗區間是 3 到 10 倍，實際看自管元件的維運複雜度）——一個資深 DBA 的年薪不便宜，而工程師花在維運自建系統上的時間是機會成本，那些時間本可以做產品。算自建划不划算，只比雲端費用而忽略人力，會得出錯誤的結論。真實的成本決策裡，這塊人力差常常比機器費用還大——有團隊把自管叢集換成託管，省下的主要不是機器錢，是叢集管理的人力與維運簡化。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>成本模型的輸入——容量怎麼算出來 → <a href="/blog/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>峰值預備的成本（為峰值留的 headroom 要付多少）→ <a href="/blog/operations/05-capacity-planning/peak-estimation/" data-link-title="峰值估算" data-link-desc="要估一個服務的峰值該準備多少時，先分清峰值屬於哪一種形狀、再用歷史倍率階梯與安全係數推算，並知道安全係數的數學根據在飽和曲線">峰值估算</a></li>
<li>完整的雲端成本治理、計費模式的深入操作 → <a href="/blog/operations/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">模組八 成本管理</a></li>
</ul>
]]></content:encoded></item><item><title>容器化資源設計</title><link>https://tarrragon.github.io/blog/operations/05-capacity-planning/container-resource-design/</link><pubDate>Fri, 03 Jul 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/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 暫停了。要確認有沒有被 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>
&lt;h2 id="磁碟-iooverlay-的寫入放大">磁碟 I/O：overlay 的寫入放大&lt;/h2>
&lt;p>容器的磁碟 I/O 有一個容易被忽略的成本：overlay filesystem 的寫入放大。Docker 的 overlay2 storage driver 把容器的寫入分層管理，每次寫入或修改檔案，overlay 在上層建立副本再改（copy-on-write）。對 SQLite 這類頻繁 fsync 的嵌入式資料庫，這層 copy-on-write 會增加 20% 到 40% 的寫入延遲——一個看起來只是「換個存放位置」的決定，實際上改變了資料庫的寫入效能。&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 暫停了。要確認有沒有被 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 要探到多深、<code>HEALTHCHECK</code> 等同 Kubernetes 的哪種 probe、readiness 與 startup 在單機 Docker 為什麼沒有對應物——是 <a href="/blog/operations/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">模組四 服務探活</a> 的主題，容器資源設計只需知道健康檢查是容器生命週期的一部分、跟資源限制一起構成「容器跑得穩」的條件。探到不健康之後的自動重啟、liveness 與 readiness 的語意分界，見 <a href="/blog/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/operations/05-capacity-planning/scaling-inflection-point/" data-link-title="規模拐點判斷" data-link-desc="判斷什麼訊號代表該擴容、什麼代表可縮容、以及該往垂直還水平擴時，用飽和曲線的三段區間、膝點的早期訊號與 ramp-up 方法回來讀">規模拐點判斷</a></li>
<li>容器的健康檢查、probe 語意與自動重啟 → <a href="/blog/operations/04-service-health/" data-link-title="模組四：服務探活與自動恢復" data-link-desc="服務掛了怎麼自動發現和恢復 — health check 設計、liveness vs readiness、systemd watchdog、process supervisor">模組四 服務探活</a></li>
<li>監控 collector 的容器部署實例 → <a href="/blog/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></channel></rss>