<?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>Cost on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cost/</link><description>Recent content in Cost on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cost/index.xml" rel="self" type="application/rss+xml"/><item><title>Retrieval Cost</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/retrieval-cost/</link><pubDate>Thu, 14 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/retrieval-cost/</guid><description>&lt;p>Retrieval cost 的核心概念是「&lt;strong>每一次 retrieve 與其周邊增強會消耗多少 latency、token、compute 與維護成本&lt;/strong>」。它讓 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG&lt;/a> 設計從「能不能找更多資料」轉成「多找這些資料是否值得」。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>Retrieval cost 橫跨 query 端、retrieval 端、context 組裝端與控制流端。它跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT&lt;/a> 有關，但不只是一個延遲數字：query rewriting 多一次 LLM call，query expansion 多次 retrieve，reranker 多一段 cross-encoder 計算，retrieved chunks 進 prompt 會增加 token cost。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>常見訊號是「accuracy 有提升，但 p95 latency 變差」「每個 query 都 retrieve，聊天問題也燒 embedding / vector DB」「multi-step retrieval 連跑三輪，答案只比 single-step 好一點」。這時問題在於收益是否大於成本，而非技術能不能做。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>判斷 retrieval cost 要把 accuracy、latency、token budget、服務費用與維運複雜度一起看。低風險聊天可用 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/adaptive-retrieval/" data-link-title="Adaptive Retrieval" data-link-desc="RAG 控制流中先判斷是否需要檢索，只在外部知識有價值時才 retrieve">adaptive retrieval&lt;/a> 降低不必要檢索；高價值問答可接受 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/reranker/" data-link-title="Reranker" data-link-desc="對 retrieval top-K 結果用 cross-encoder 重新排序的 RAG 第二階段、品質提升顯著但 latency / cost 增加">reranker&lt;/a> 或 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/multi-step-retrieval/" data-link-title="Multi-Step Retrieval" data-link-desc="RAG 中多輪 retrieve → 判斷 → 再 retrieve 的控制流，用來處理 multi-hop 問題">multi-step retrieval&lt;/a> 的額外成本；即時補完則通常偏向 single-step、cache 或較小 top-k。&lt;/p></description><content:encoded><![CDATA[<p>Retrieval cost 的核心概念是「<strong>每一次 retrieve 與其周邊增強會消耗多少 latency、token、compute 與維護成本</strong>」。它讓 <a href="/blog/llm/knowledge-cards/rag/" data-link-title="RAG" data-link-desc="Retrieval-Augmented Generation：動態外掛知識給 LLM、繞開模型參數記憶的靜態限制">RAG</a> 設計從「能不能找更多資料」轉成「多找這些資料是否值得」。</p>
<h2 id="概念位置">概念位置</h2>
<p>Retrieval cost 橫跨 query 端、retrieval 端、context 組裝端與控制流端。它跟 <a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a> 有關，但不只是一個延遲數字：query rewriting 多一次 LLM call，query expansion 多次 retrieve，reranker 多一段 cross-encoder 計算，retrieved chunks 進 prompt 會增加 token cost。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>常見訊號是「accuracy 有提升，但 p95 latency 變差」「每個 query 都 retrieve，聊天問題也燒 embedding / vector DB」「multi-step retrieval 連跑三輪，答案只比 single-step 好一點」。這時問題在於收益是否大於成本，而非技術能不能做。</p>
<h2 id="設計責任">設計責任</h2>
<p>判斷 retrieval cost 要把 accuracy、latency、token budget、服務費用與維運複雜度一起看。低風險聊天可用 <a href="/blog/llm/knowledge-cards/adaptive-retrieval/" data-link-title="Adaptive Retrieval" data-link-desc="RAG 控制流中先判斷是否需要檢索，只在外部知識有價值時才 retrieve">adaptive retrieval</a> 降低不必要檢索；高價值問答可接受 <a href="/blog/llm/knowledge-cards/reranker/" data-link-title="Reranker" data-link-desc="對 retrieval top-K 結果用 cross-encoder 重新排序的 RAG 第二階段、品質提升顯著但 latency / cost 增加">reranker</a> 或 <a href="/blog/llm/knowledge-cards/multi-step-retrieval/" data-link-title="Multi-Step Retrieval" data-link-desc="RAG 中多輪 retrieve → 判斷 → 再 retrieve 的控制流，用來處理 multi-hop 問題">multi-step retrieval</a> 的額外成本；即時補完則通常偏向 single-step、cache 或較小 top-k。</p>
]]></content:encoded></item><item><title>Prompt Cache</title><link>https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-cache/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-cache/</guid><description>&lt;p>Prompt cache 的核心概念是「&lt;strong>LLM 服務端 / 推論伺服器把重複出現的 prompt prefix（如 system prompt + tool schema）的 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> 暫存起來、後續 query 跳過該 prefix 的 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill&lt;/a> 階段&lt;/strong>」。Anthropic / OpenAI / Bedrock / Gemini 都提供、最高 90% cost 折扣 + 13-31% &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT&lt;/a> 改善、是 coding agent / long-context 應用的核心 cost / latency 槓桿。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>跟既有 cache 概念的層次：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Cache 層&lt;/th>
 &lt;th>範圍&lt;/th>
 &lt;th>機制&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a>&lt;/td>
 &lt;td>單一 conversation 的同一次推論&lt;/td>
 &lt;td>過去 token 的 K/V 暫存、autoregressive 才省重算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">Prefix cache&lt;/a>&lt;/td>
 &lt;td>多 request 共用 prefix（同 server 同 model）&lt;/td>
 &lt;td>跨 request 共用 KV cache、production 推論伺服器特性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Prompt cache（本卡）&lt;/strong>&lt;/td>
 &lt;td>跨 request 跨時間、雲端 LLM API 服務端&lt;/td>
 &lt;td>服務端把 prefix 的 KV cache 持久化、有 TTL&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Prompt cache 的「保留範圍」跟「定價」是商業 LLM 的 product feature：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務&lt;/th>
 &lt;th>Cache TTL&lt;/th>
 &lt;th>Write cost&lt;/th>
 &lt;th>Read cost&lt;/th>
 &lt;th>觸發方式&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Anthropic（cache_control）&lt;/td>
 &lt;td>5min 預設、1h ext&lt;/td>
 &lt;td>1.25× 原價&lt;/td>
 &lt;td>0.1× 原價（90% 折扣）&lt;/td>
 &lt;td>明確 cache_control breakpoint&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>OpenAI&lt;/td>
 &lt;td>自動（隱式）&lt;/td>
 &lt;td>同原價&lt;/td>
 &lt;td>0.5× 原價（50% 折扣）&lt;/td>
 &lt;td>自動偵測重複 prefix（&amp;gt; 1024 token）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Bedrock（Anthropic）&lt;/td>
 &lt;td>5min&lt;/td>
 &lt;td>同 Anthropic&lt;/td>
 &lt;td>同上&lt;/td>
 &lt;td>同 Anthropic&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Gemini&lt;/td>
 &lt;td>自動 + explicit&lt;/td>
 &lt;td>視方案&lt;/td>
 &lt;td>視方案&lt;/td>
 &lt;td>implicit + context caching API&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;blockquote>
&lt;p>&lt;strong>事實查核註&lt;/strong>：定價跟 TTL 隨時間更新、引用前以對應 vendor 當前文件為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>讀 LLM API docs / coding agent 設計 / cost optimization blog 看到「prompt cache」「context caching」「cache_control」就是這機制。寫 code 場景的判讀：&lt;/p></description><content:encoded><![CDATA[<p>Prompt cache 的核心概念是「<strong>LLM 服務端 / 推論伺服器把重複出現的 prompt prefix（如 system prompt + tool schema）的 <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 暫存起來、後續 query 跳過該 prefix 的 <a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a> 階段</strong>」。Anthropic / OpenAI / Bedrock / Gemini 都提供、最高 90% cost 折扣 + 13-31% <a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a> 改善、是 coding agent / long-context 應用的核心 cost / latency 槓桿。</p>
<h2 id="概念位置">概念位置</h2>
<p>跟既有 cache 概念的層次：</p>
<table>
  <thead>
      <tr>
          <th>Cache 層</th>
          <th>範圍</th>
          <th>機制</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a></td>
          <td>單一 conversation 的同一次推論</td>
          <td>過去 token 的 K/V 暫存、autoregressive 才省重算</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">Prefix cache</a></td>
          <td>多 request 共用 prefix（同 server 同 model）</td>
          <td>跨 request 共用 KV cache、production 推論伺服器特性</td>
      </tr>
      <tr>
          <td><strong>Prompt cache（本卡）</strong></td>
          <td>跨 request 跨時間、雲端 LLM API 服務端</td>
          <td>服務端把 prefix 的 KV cache 持久化、有 TTL</td>
      </tr>
  </tbody>
</table>
<p>Prompt cache 的「保留範圍」跟「定價」是商業 LLM 的 product feature：</p>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>Cache TTL</th>
          <th>Write cost</th>
          <th>Read cost</th>
          <th>觸發方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Anthropic（cache_control）</td>
          <td>5min 預設、1h ext</td>
          <td>1.25× 原價</td>
          <td>0.1× 原價（90% 折扣）</td>
          <td>明確 cache_control breakpoint</td>
      </tr>
      <tr>
          <td>OpenAI</td>
          <td>自動（隱式）</td>
          <td>同原價</td>
          <td>0.5× 原價（50% 折扣）</td>
          <td>自動偵測重複 prefix（&gt; 1024 token）</td>
      </tr>
      <tr>
          <td>Bedrock（Anthropic）</td>
          <td>5min</td>
          <td>同 Anthropic</td>
          <td>同上</td>
          <td>同 Anthropic</td>
      </tr>
      <tr>
          <td>Gemini</td>
          <td>自動 + explicit</td>
          <td>視方案</td>
          <td>視方案</td>
          <td>implicit + context caching API</td>
      </tr>
  </tbody>
</table>
<blockquote>
<p><strong>事實查核註</strong>：定價跟 TTL 隨時間更新、引用前以對應 vendor 當前文件為準。</p></blockquote>
<h2 id="設計責任">設計責任</h2>
<p>讀 LLM API docs / coding agent 設計 / cost optimization blog 看到「prompt cache」「context caching」「cache_control」就是這機制。寫 code 場景的判讀：</p>
<ol>
<li><strong>誰最值得開</strong>：coding agent（system prompt + tool schema 經常 &gt; 10K token、每 turn 重用）、long-context RAG（檢索 chunks 重用）、long conversation（history 累積）</li>
<li><strong>設計原則</strong>：把不變的內容（system prompt、tool schema、固定文件）放 prefix；變動的（user query、最新 file content）放後面</li>
<li><strong>常見 anti-pattern</strong>：在 prefix 插入 timestamp / user-id / request-id → 每次 prefix 不同 → cache 從不命中、付 1.25× write cost 沒得回本</li>
<li><strong>5 分鐘 TTL 的意涵</strong>：query 之間間隔 &gt; 5 分鐘、cache 已 expire、要 1h ext TTL 才能撐長對話</li>
<li><strong>跟 <a href="/blog/llm/knowledge-cards/context-budget/" data-link-title="Context Budget" data-link-desc="Coding agent 的 context window 拆分配額：system prompt &#43; tool schema &#43; history &#43; file content &#43; reasoning &#43; tool result 各佔多少、留多少 margin">context budget</a> 的關係</strong>：cache 攤平 scaffold 部分的 cost、所以可以放寬「scaffold ≤ 25%」的成本顧慮、focus 在「不超 context limit」即可</li>
</ol>
]]></content:encoded></item><item><title>成本可見性與最小可行治理節奏</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/cost-visibility-rhythm/</guid><description>&lt;p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源歸屬靠 tagging、密鑰安全靠 secret 管理（見 &lt;a href="https://tarrragon.github.io/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">tagging 與 secrets&lt;/a>），本篇處理兩個後續問題：成本怎麼拆解到擁有者，以及治理規範的節奏怎麼拿捏 — 什麼該第一天就立、什麼等到痛點出現再加。&lt;/p>
&lt;p>先界定邊界。成本這一塊分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這裡，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的範圍。&lt;/p>
&lt;h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途&lt;/h2>
&lt;p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。&lt;/p>
&lt;h3 id="tag-驅動的成本分攤">Tag 驅動的成本分攤&lt;/h3>
&lt;p>把成本拆解到擁有者的地基，正是 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤（Cost Allocation Tag）」。啟用是一次性設定，之後新建的資源只要帶了這個 tag，費用就會自動歸入對應維度。&lt;/p>
&lt;p>啟用後，&lt;code>cost-center&lt;/code> 和 &lt;code>owner&lt;/code> 就從單純的標籤升級成帳單的可查詢維度：&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">&lt;span class="c1"># 用 AWS CLI 查某個 cost-center 的月費用&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws ce get-cost-and-usage &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --time-period &lt;span class="nv">Start&lt;/span>&lt;span class="o">=&lt;/span>2026-06-01,End&lt;span class="o">=&lt;/span>2026-06-30 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --granularity MONTHLY &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --filter &lt;span class="s1">&amp;#39;{&amp;#34;Tags&amp;#34;:{&amp;#34;Key&amp;#34;:&amp;#34;cost-center&amp;#34;,&amp;#34;Values&amp;#34;:[&amp;#34;cc-1024&amp;#34;]}}&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --metrics BlendedCost &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --group-by &lt;span class="nv">Type&lt;/span>&lt;span class="o">=&lt;/span>TAG,Key&lt;span class="o">=&lt;/span>owner&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。&lt;/p>
&lt;h3 id="成本異常告警">成本異常告警&lt;/h3>
&lt;p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。在可見性建立之後，下一步是設一條成本異常告警：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_ce_anomaly_monitor&amp;#34; &amp;#34;cost&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;daily-cost-anomaly&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> monitor_type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;DIMENSIONAL&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> monitor_dimension&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;SERVICE&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_ce_anomaly_subscription&amp;#34; &amp;#34;alert&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cost-anomaly-alert&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> frequency&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;DAILY&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> monitor_arn_list&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_ce_anomaly_monitor&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&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>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">subscriber&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n"> type&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;SNS&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n"> address&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_sns_topic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">cost_alerts&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl"> &lt;span class="k">threshold_expression&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl"> &lt;span class="k">dimension&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="n"> key&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ANOMALY_TOTAL_IMPACT_ABSOLUTE&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl">&lt;span class="n"> values&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;100&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">&lt;span class="n"> match_options&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;GREATER_THAN_OR_EQUAL&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">23&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">24&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">25&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>當告警觸發時，因為有 tag，可以立刻定位是哪個團隊的哪類資源在漲，而不是面對一個無法拆解到具體團隊或資源類型的總數。常見的成本異常來源：開發者開了一組大型 instance 測試後忘了關、某個 auto-scaling group 的最大值設太高在流量尖峰長出了大量機器、NAT Gateway 被大量出站流量灌到帳單翻倍。這些情境只要 tag 到位，都能在異常告警觸發後幾分鐘內找到根因。&lt;/p>
&lt;p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。&lt;/p>
&lt;p>成本治理在不同規模下的操作形態差異很大。Netflix 把多套關聯式資料庫統一到 Aurora 後成本下降 28%，核心操作是「把資源種類收斂、讓成本歸因的維度減少」——這在 tagging 已經到位的前提下才做得到，見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：Aurora 整併&lt;/a>。另一個極端是 Arcjet 用 Redis Streams 取代 managed Kafka，年費從六位數美金降到約 $1k，代價是自行維護 retention 與 consumer group 監控——這個取捨的前提是團隊有能力承擔額外的運維面，見 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">3.C43 Arcjet：Redis Streams 取代 Kafka&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源歸屬靠 tagging、密鑰安全靠 secret 管理（見 <a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">tagging 與 secrets</a>），本篇處理兩個後續問題：成本怎麼拆解到擁有者，以及治理規範的節奏怎麼拿捏 — 什麼該第一天就立、什麼等到痛點出現再加。</p>
<p>先界定邊界。成本這一塊分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這裡，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的範圍。</p>
<h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途</h2>
<p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。</p>
<h3 id="tag-驅動的成本分攤">Tag 驅動的成本分攤</h3>
<p>把成本拆解到擁有者的地基，正是 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤（Cost Allocation Tag）」。啟用是一次性設定，之後新建的資源只要帶了這個 tag，費用就會自動歸入對應維度。</p>
<p>啟用後，<code>cost-center</code> 和 <code>owner</code> 就從單純的標籤升級成帳單的可查詢維度：</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"><span class="c1"># 用 AWS CLI 查某個 cost-center 的月費用</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws ce get-cost-and-usage <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --time-period <span class="nv">Start</span><span class="o">=</span>2026-06-01,End<span class="o">=</span>2026-06-30 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --granularity MONTHLY <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;{&#34;Tags&#34;:{&#34;Key&#34;:&#34;cost-center&#34;,&#34;Values&#34;:[&#34;cc-1024&#34;]}}&#39;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --metrics BlendedCost <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --group-by <span class="nv">Type</span><span class="o">=</span>TAG,Key<span class="o">=</span>owner</span></span></code></pre></div><p>「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。</p>
<h3 id="成本異常告警">成本異常告警</h3>
<p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。在可見性建立之後，下一步是設一條成本異常告警：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_ce_anomaly_monitor&#34; &#34;cost&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;daily-cost-anomaly&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  monitor_type</span>      <span class="o">=</span> <span class="s2">&#34;DIMENSIONAL&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  monitor_dimension</span> <span class="o">=</span> <span class="s2">&#34;SERVICE&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_ce_anomaly_subscription&#34; &#34;alert&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  name</span>      <span class="o">=</span> <span class="s2">&#34;cost-anomaly-alert&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  frequency</span> <span class="o">=</span> <span class="s2">&#34;DAILY&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  monitor_arn_list</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_ce_anomaly_monitor</span><span class="p">.</span><span class="k">cost</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">  <span class="k">subscriber</span> {
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">    type</span>    <span class="o">=</span> <span class="s2">&#34;SNS&#34;</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">    address</span> <span class="o">=</span> <span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">cost_alerts</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">  }
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">  <span class="k">threshold_expression</span> {
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">dimension</span> {
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">      key</span>           <span class="o">=</span> <span class="s2">&#34;ANOMALY_TOTAL_IMPACT_ABSOLUTE&#34;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">      values</span>        <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;100&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="n">      match_options</span> <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;GREATER_THAN_OR_EQUAL&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    }
</span></span><span class="line"><span class="ln">24</span><span class="cl">  }
</span></span><span class="line"><span class="ln">25</span><span class="cl">}</span></span></code></pre></div><p>當告警觸發時，因為有 tag，可以立刻定位是哪個團隊的哪類資源在漲，而不是面對一個無法拆解到具體團隊或資源類型的總數。常見的成本異常來源：開發者開了一組大型 instance 測試後忘了關、某個 auto-scaling group 的最大值設太高在流量尖峰長出了大量機器、NAT Gateway 被大量出站流量灌到帳單翻倍。這些情境只要 tag 到位，都能在異常告警觸發後幾分鐘內找到根因。</p>
<p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。</p>
<p>成本治理在不同規模下的操作形態差異很大。Netflix 把多套關聯式資料庫統一到 Aurora 後成本下降 28%，核心操作是「把資源種類收斂、讓成本歸因的維度減少」——這在 tagging 已經到位的前提下才做得到，見 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix：Aurora 整併</a>。另一個極端是 Arcjet 用 Redis Streams 取代 managed Kafka，年費從六位數美金降到約 $1k，代價是自行維護 retention 與 consumer group 監控——這個取捨的前提是團隊有能力承擔額外的運維面，見 <a href="/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">3.C43 Arcjet：Redis Streams 取代 Kafka</a>。</p>
<h2 id="最小可行節奏先把地基跑起來再逐步加">最小可行節奏：先把地基跑起來，再逐步加</h2>
<p>治理的最小可行節奏，是早期只立「拔掉就會痛、補起來很貴」的那幾條規範，其餘留到規模逼出需求時再加。治理機制本身有維護成本 — 每一條策略規則、每一個審批關卡、每一套標籤分類法都要有人維護、有人解釋、有人在它擋錯東西時來救。在團隊還小、資源還少時堆滿企業級治理框架，付出的是當下的速度，換來的是一套還用不到的複雜度。</p>
<h3 id="補救成本曲線">補救成本曲線</h3>
<p>判斷一條治理規範該不該現在就立，看它的「補救成本曲線」— 越晚導入、事後補救的代價越高的規範，越應該提前立：</p>
<table>
  <thead>
      <tr>
          <th>規範</th>
          <th>補救成本曲線</th>
          <th>day-1 該立</th>
          <th>說明</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Tagging</td>
          <td>陡峭</td>
          <td>是</td>
          <td>幾百個沒 tag 的資源要回頭考古，建立時順手標只要幾秒</td>
      </tr>
      <tr>
          <td>Secrets 不進 code</td>
          <td>幾乎垂直</td>
          <td>是</td>
          <td>密鑰一旦進了 git 歷史就無法清除，只能輪替</td>
      </tr>
      <tr>
          <td>成本分攤維度</td>
          <td>中等</td>
          <td>是（輕量）</td>
          <td>依賴 tagging，tag 立了它就近乎免費啟用</td>
      </tr>
      <tr>
          <td>Secret 自動輪替</td>
          <td>平緩</td>
          <td>等</td>
          <td>手動輪替在早期可接受，自動化在 secret 數量增多後再投入</td>
      </tr>
      <tr>
          <td>細緻的審批流程</td>
          <td>平坦</td>
          <td>等</td>
          <td>補救成本低、可以隨時加，早期硬上反而拖慢交付</td>
      </tr>
      <tr>
          <td>多層級策略引擎（OPA / Sentinel）</td>
          <td>平坦</td>
          <td>等</td>
          <td>等到 tag policy 擋不住的邊界案例出現再引入</td>
      </tr>
  </tbody>
</table>
<p>這個曲線給出的節奏是：補救成本陡的從第一天就用 IaC 強制，補救成本平的等到痛點確實出現 — 開始有人手滑誤刪、開始有跨團隊的權限爭議 — 再有針對性地加。那時你也才知道該往哪個方向加。</p>
<h3 id="過度治理的訊號">過度治理的訊號</h3>
<p>過度治理跟過度設計是同一類問題，訊號很類似：</p>
<ul>
<li>建一個測試用的小資源需要走三層審批流程</li>
<li>團隊花在解釋為什麼某個護欄擋錯的時間，比護欄實際擋住的風險還多</li>
<li>策略規則的 exception 清單比規則本身還長</li>
<li>新人第一週的大部分時間花在理解治理框架而非理解業務</li>
</ul>
<p>這些訊號出現時，該回頭簡化 — 砍掉沒帶來價值的規則、把誤判率高的規則降級為 warning 而非 blocking。治理框架跟程式碼一樣需要重構。</p>
<h3 id="和其他模組的節奏對齊">和其他模組的節奏對齊</h3>
<p>這個節奏跟<a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零</a>的成熟度階梯是同一套思路：基礎設施的治理跟基礎設施本身一樣，是逐級長出來的，不是一次到位設計完的。把規範變成自動護欄的工程（PR 階段擋缺 tag、CI 掃 secret）值得早投入，因為自動化的護欄維護成本低、且越早接管越省人力 — 這部分怎麼落地在<a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a> 展開。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/00-infra-mindset/" data-link-title="模組零：infra 是什麼，為什麼 day 1 就要鋪地基" data-link-desc="基礎設施的責任邊界、成熟度階梯，以及地基為什麼總在環境爆炸時才被看見">模組零：infra 是什麼</a>：成熟度階梯的務實節奏思路</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：tag 合規與 secret 掃描整合進 CI pipeline</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：運行期的成本控制與優化手段</li>
</ul>
]]></content:encoded></item><item><title>成本判斷表</title><link>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/cost-judgment/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/testing/03-protocol-integration-test/cost-judgment/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test&lt;/a> 的價值在於用自動化方式驗證 &lt;a href="https://tarrragon.github.io/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽&lt;/a>的協議層盲區。但它有建置成本（服務 fixture 管理）和維護成本（服務更新時 test 要跟著改）。判斷是否值得投資，依據的是兩個維度：服務啟動成本和協議複雜度。&lt;/p>
&lt;h2 id="服務啟動成本">服務啟動成本&lt;/h2>
&lt;p>服務啟動成本決定了 protocol integration test 的執行成本 — test 跑一次要多久、CI 中佔多少時間。&lt;/p>
&lt;h3 id="極低成本同機單程序">極低成本（同機單程序）&lt;/h3>
&lt;p>Server 是一個本機程序，&lt;code>Process.start&lt;/code> 一行啟動，不需要 Docker、不需要網路、不需要設定檔。啟動到 ready 不到 2 秒。&lt;/p>
&lt;p>app_tunnel 的 ttyd 就是這個場景。&lt;code>ttyd bash&lt;/code> 在本機啟動，WebSocket 服務立即可用。整個 protocol integration test suite 的額外成本約 10-15 秒（包含啟動、健康檢查、5 個 test 各 2 秒）（本章合成，TF-8 Derive）。&lt;/p>
&lt;p>在這個成本等級下，protocol integration test 幾乎沒有理由不寫。&lt;/p>
&lt;h3 id="低成本docker-單容器">低成本（Docker 單容器）&lt;/h3>
&lt;p>Server 用 Docker 容器啟動，需要 pull image（首次或更新時），啟動到 ready 約 5-30 秒。Redis、PostgreSQL、Elasticsearch 等 open source 服務屬於這個等級。&lt;/p>
&lt;p>CI 中用 image cache 可以把 pull 時間降到接近零。但容器啟動時間仍比原生程序長。整個 protocol integration test suite 的額外成本約 30-60 秒。&lt;/p>
&lt;p>在這個成本等級下，如果協議有任何複雜度（見下方），protocol integration test 值得寫。&lt;/p>
&lt;h3 id="中等成本多容器堆疊">中等成本（多容器堆疊）&lt;/h3>
&lt;p>Server 依賴多個服務（app server + database + cache + message queue），需要 Docker Compose 管理。啟動到所有服務 ready 約 30-120 秒。&lt;/p>
&lt;p>Protocol integration test 的執行成本顯著上升。適合在 CI 的獨立 stage 跑（和 unit test 分開），避免拖慢 fast feedback loop。&lt;/p>
&lt;h3 id="高成本外部服務--saas">高成本（外部服務 / SaaS）&lt;/h3>
&lt;p>Server 是外部 SaaS（Stripe API、AWS S3、第三方 OAuth provider），無法本地啟動。Test 需要打到 sandbox environment，有速率限制和網路延遲。&lt;/p>
&lt;p>在這個成本等級下，consumer-driven contract test 可能比 protocol integration test 更實用 — 用 contract 定義期望的 request/response，在本地驗證 client 端行為，不需要每次都打到外部服務。&lt;/p>
&lt;h2 id="協議複雜度">協議複雜度&lt;/h2>
&lt;p>協議複雜度決定了 mock 遮蔽的風險大小 — 風險越大，protocol integration test 的價值越高。&lt;/p>
&lt;p>&lt;strong>高複雜度&lt;/strong>：WebSocket（frame type、handshake、子協議）、gRPC（streaming、deadline、metadata）、MQTT（QoS level、retain、will message）。API 簽名隱藏了協議層的行為分支，mock 結構性地無法覆蓋。&lt;/p>
&lt;p>&lt;strong>中複雜度&lt;/strong>：HTTP REST API（多種 status code、error body 格式、認證流程、分頁）。核心語意（JSON request/response）差距小，但 edge case（error response 格式、header 要求）仍可能被 mock 遮蔽。&lt;/p>
&lt;p>&lt;strong>低複雜度&lt;/strong>：本地 IPC（Unix socket、named pipe）、標準格式的檔案讀寫。協議行為簡單，mock 和真實行為差距小。&lt;/p>
&lt;h2 id="判斷矩陣">判斷矩陣&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務啟動成本&lt;/th>
 &lt;th>協議複雜度高&lt;/th>
 &lt;th>協議複雜度中&lt;/th>
 &lt;th>協議複雜度低&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>極低&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>低&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>可選&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>中&lt;/td>
 &lt;td>protocol test&lt;/td>
 &lt;td>視 mock 寬鬆度決定&lt;/td>
 &lt;td>實機測試替代&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高&lt;/td>
 &lt;td>contract test + 實機&lt;/td>
 &lt;td>contract test&lt;/td>
 &lt;td>實機測試替代&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>「可選」代表 protocol integration test 有價值但不是必要 — 實機測試階段的手動驗證可能足夠。「實機測試替代」代表成本太高或收益太低，依賴實機測試階段的人工驗證。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/testing/knowledge-cards/protocol-integration-test/" data-link-title="Protocol Integration Test" data-link-desc="驗證程式碼和真實外部服務之間的協議互動是否正確的 test 層級">Protocol integration test</a> 的價值在於用自動化方式驗證 <a href="/blog/testing/knowledge-cards/mock-masking/" data-link-title="Mock 遮蔽" data-link-desc="mock 模擬 API 層但不模擬協議層，造成的結構性驗證盲區">mock 遮蔽</a>的協議層盲區。但它有建置成本（服務 fixture 管理）和維護成本（服務更新時 test 要跟著改）。判斷是否值得投資，依據的是兩個維度：服務啟動成本和協議複雜度。</p>
<h2 id="服務啟動成本">服務啟動成本</h2>
<p>服務啟動成本決定了 protocol integration test 的執行成本 — test 跑一次要多久、CI 中佔多少時間。</p>
<h3 id="極低成本同機單程序">極低成本（同機單程序）</h3>
<p>Server 是一個本機程序，<code>Process.start</code> 一行啟動，不需要 Docker、不需要網路、不需要設定檔。啟動到 ready 不到 2 秒。</p>
<p>app_tunnel 的 ttyd 就是這個場景。<code>ttyd bash</code> 在本機啟動，WebSocket 服務立即可用。整個 protocol integration test suite 的額外成本約 10-15 秒（包含啟動、健康檢查、5 個 test 各 2 秒）（本章合成，TF-8 Derive）。</p>
<p>在這個成本等級下，protocol integration test 幾乎沒有理由不寫。</p>
<h3 id="低成本docker-單容器">低成本（Docker 單容器）</h3>
<p>Server 用 Docker 容器啟動，需要 pull image（首次或更新時），啟動到 ready 約 5-30 秒。Redis、PostgreSQL、Elasticsearch 等 open source 服務屬於這個等級。</p>
<p>CI 中用 image cache 可以把 pull 時間降到接近零。但容器啟動時間仍比原生程序長。整個 protocol integration test suite 的額外成本約 30-60 秒。</p>
<p>在這個成本等級下，如果協議有任何複雜度（見下方），protocol integration test 值得寫。</p>
<h3 id="中等成本多容器堆疊">中等成本（多容器堆疊）</h3>
<p>Server 依賴多個服務（app server + database + cache + message queue），需要 Docker Compose 管理。啟動到所有服務 ready 約 30-120 秒。</p>
<p>Protocol integration test 的執行成本顯著上升。適合在 CI 的獨立 stage 跑（和 unit test 分開），避免拖慢 fast feedback loop。</p>
<h3 id="高成本外部服務--saas">高成本（外部服務 / SaaS）</h3>
<p>Server 是外部 SaaS（Stripe API、AWS S3、第三方 OAuth provider），無法本地啟動。Test 需要打到 sandbox environment，有速率限制和網路延遲。</p>
<p>在這個成本等級下，consumer-driven contract test 可能比 protocol integration test 更實用 — 用 contract 定義期望的 request/response，在本地驗證 client 端行為，不需要每次都打到外部服務。</p>
<h2 id="協議複雜度">協議複雜度</h2>
<p>協議複雜度決定了 mock 遮蔽的風險大小 — 風險越大，protocol integration test 的價值越高。</p>
<p><strong>高複雜度</strong>：WebSocket（frame type、handshake、子協議）、gRPC（streaming、deadline、metadata）、MQTT（QoS level、retain、will message）。API 簽名隱藏了協議層的行為分支，mock 結構性地無法覆蓋。</p>
<p><strong>中複雜度</strong>：HTTP REST API（多種 status code、error body 格式、認證流程、分頁）。核心語意（JSON request/response）差距小，但 edge case（error response 格式、header 要求）仍可能被 mock 遮蔽。</p>
<p><strong>低複雜度</strong>：本地 IPC（Unix socket、named pipe）、標準格式的檔案讀寫。協議行為簡單，mock 和真實行為差距小。</p>
<h2 id="判斷矩陣">判斷矩陣</h2>
<table>
  <thead>
      <tr>
          <th>服務啟動成本</th>
          <th>協議複雜度高</th>
          <th>協議複雜度中</th>
          <th>協議複雜度低</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>極低</td>
          <td>protocol test</td>
          <td>protocol test</td>
          <td>protocol test</td>
      </tr>
      <tr>
          <td>低</td>
          <td>protocol test</td>
          <td>protocol test</td>
          <td>可選</td>
      </tr>
      <tr>
          <td>中</td>
          <td>protocol test</td>
          <td>視 mock 寬鬆度決定</td>
          <td>實機測試替代</td>
      </tr>
      <tr>
          <td>高</td>
          <td>contract test + 實機</td>
          <td>contract test</td>
          <td>實機測試替代</td>
      </tr>
  </tbody>
</table>
<p>「可選」代表 protocol integration test 有價值但不是必要 — 實機測試階段的手動驗證可能足夠。「實機測試替代」代表成本太高或收益太低，依賴實機測試階段的人工驗證。</p>
<p>成本和複雜度的評估結果決定了要建什麼等級的 test 基礎設施。<a href="/blog/testing/03-protocol-integration-test/definition-and-boundary/" data-link-title="Protocol integration test 定義" data-link-desc="Protocol integration test 和 unit test / E2E test 的邊界 — 驗證程式碼和真實服務的協議契約，不驗證 UI 也不用 mock">Protocol integration test 定義</a>提供這一層 test 的精確邊界，<a href="/blog/testing/01-test-strategy-layers/when-protocol-integration-test/" data-link-title="判斷原則：什麼時候需要 protocol integration test" data-link-desc="從服務架構特徵判斷是否需要 protocol integration test 的決策流程 — 協議複雜度、mock 寬鬆度、失敗靜默度三個維度">testing 模組一的判斷原則</a>從 mock 遮蔽角度補充另一個判斷維度。決定要建之後，<a href="/blog/testing/03-protocol-integration-test/service-fixture-management/" data-link-title="CI 中的服務 fixture 管理" data-link-desc="在 CI 中啟動和停止真實服務的 test harness 設計 — Process.start / Docker / testcontainers 三種方案的適用場景">CI 中的服務 fixture 管理</a>處理啟動和停止真實服務的工程問題。</p>
]]></content:encoded></item><item><title>ECS Fargate 成本分析與優化</title><link>https://tarrragon.github.io/blog/infra/05-core-services/ecs-fargate-cost-optimization/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/05-core-services/ecs-fargate-cost-optimization/</guid><description>&lt;p>Fargate 把運算的維運面外包給 AWS — 不需要管 EC2 instance、不需要管 AMI 更新、不需要管 capacity provider 的擴縮邏輯。這份簡化的代價是單位成本較高。當服務規模小或流量不穩定時，Fargate 的簡化值回票價；當服務規模穩定且持續運行時，EC2 launch type 的單位成本優勢會累積到值得切換的量級。本篇的目標是讓讀者能判斷自己的服務在成本曲線的哪個位置、以及有哪些槓桿可以調。&lt;/p>
&lt;h2 id="fargate-計價模型">Fargate 計價模型&lt;/h2>
&lt;p>Fargate 按 task 的 vCPU 時數和記憶體時數分別計費，從 task 啟動（pull image 完成、進入 RUNNING）到停止。計費的最小粒度是一分鐘，不足一分鐘按一分鐘算。&lt;/p>
&lt;p>以 ap-northeast-1（東京）為例的單價（截至撰寫時的量級參考，實際以 AWS 定價頁為準）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資源&lt;/th>
 &lt;th>單價（每小時）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1 vCPU&lt;/td>
 &lt;td>~$0.05056&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>1 GB RAM&lt;/td>
 &lt;td>~$0.00553&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>一個 1 vCPU / 2 GB 的 task 持續運行一個月（730 小時）的費用約為 $0.05056 × 730 + $0.00553 × 2 × 730 ≈ $44.97。這個數字是所有後續比較的基線。&lt;/p>
&lt;p>Fargate 的計費粒度還有一個常被忽略的面向：task 規格只能從 AWS 預定義的 vCPU/memory 組合中選。如果應用只需要 0.3 vCPU / 512 MB，最小可選的配置是 0.25 vCPU / 0.5 GB，但如果需要 0.3 vCPU / 1 GB，就得選 0.5 vCPU / 1 GB — 多付了 0.2 vCPU 的費用。這個「階梯式浪費」在小規格 task 上比例最高。&lt;/p>
&lt;h2 id="fargate-vs-ec2-launch-type-的成本比較">Fargate vs EC2 launch type 的成本比較&lt;/h2>
&lt;p>EC2 launch type 的成本結構不同：付的是 EC2 instance 的時數（不管上面跑幾個 task），加上 ECS 本身不收費。省的是 Fargate 的 markup，多的是 instance 管理（AMI 更新、capacity provider 設定、instance 閒置時仍計費）。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>場景&lt;/th>
 &lt;th>Fargate 月費&lt;/th>
 &lt;th>EC2（t3.medium）月費&lt;/th>
 &lt;th>差異&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>1 task, 1 vCPU / 2 GB, 持續&lt;/td>
 &lt;td>~$45&lt;/td>
 &lt;td>~$30（共享 instance）&lt;/td>
 &lt;td>+50%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>5 tasks, 各 0.5 vCPU / 1 GB&lt;/td>
 &lt;td>~$113&lt;/td>
 &lt;td>~$30（1 台 t3.medium 裝得下）&lt;/td>
 &lt;td>+277%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>20 tasks, 各 1 vCPU / 2 GB&lt;/td>
 &lt;td>~$900&lt;/td>
 &lt;td>~$240（4 台 t3.xlarge）&lt;/td>
 &lt;td>+275%&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>流量波動大，尖峰 10 tasks / 離峰 1&lt;/td>
 &lt;td>~$180（加權平均）&lt;/td>
 &lt;td>~$150（需預留尖峰容量）&lt;/td>
 &lt;td>+20%&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>幾個判讀要點：&lt;/p>
&lt;ul>
&lt;li>task 數量少且持續運行時，Fargate 的溢價比例最高（+50% 到 +277%），但絕對金額小（$15-$80/月的差距），不值得為此承擔 instance 管理的維運負擔&lt;/li>
&lt;li>task 數量多且持續運行時，EC2 的絕對節省量開始可觀（$660/月），這時候切換的維運成本有回報&lt;/li>
&lt;li>流量波動大時，Fargate 的優勢是按需計費 — 離峰時 task 數降下來就停止計費，EC2 instance 閒置時仍然計費。波動越大，Fargate 的成本效益越接近或超過 EC2&lt;/li>
&lt;/ul>
&lt;h2 id="fargate-spot">Fargate Spot&lt;/h2>
&lt;p>Fargate Spot 使用 AWS 的閒置容量，價格約為 on-demand 的 30%（折扣幅度 ~70%），代價是 AWS 可以隨時回收容量、task 會收到 SIGTERM 後被終止。&lt;/p></description><content:encoded><![CDATA[<p>Fargate 把運算的維運面外包給 AWS — 不需要管 EC2 instance、不需要管 AMI 更新、不需要管 capacity provider 的擴縮邏輯。這份簡化的代價是單位成本較高。當服務規模小或流量不穩定時，Fargate 的簡化值回票價；當服務規模穩定且持續運行時，EC2 launch type 的單位成本優勢會累積到值得切換的量級。本篇的目標是讓讀者能判斷自己的服務在成本曲線的哪個位置、以及有哪些槓桿可以調。</p>
<h2 id="fargate-計價模型">Fargate 計價模型</h2>
<p>Fargate 按 task 的 vCPU 時數和記憶體時數分別計費，從 task 啟動（pull image 完成、進入 RUNNING）到停止。計費的最小粒度是一分鐘，不足一分鐘按一分鐘算。</p>
<p>以 ap-northeast-1（東京）為例的單價（截至撰寫時的量級參考，實際以 AWS 定價頁為準）：</p>
<table>
  <thead>
      <tr>
          <th>資源</th>
          <th>單價（每小時）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 vCPU</td>
          <td>~$0.05056</td>
      </tr>
      <tr>
          <td>1 GB RAM</td>
          <td>~$0.00553</td>
      </tr>
  </tbody>
</table>
<p>一個 1 vCPU / 2 GB 的 task 持續運行一個月（730 小時）的費用約為 $0.05056 × 730 + $0.00553 × 2 × 730 ≈ $44.97。這個數字是所有後續比較的基線。</p>
<p>Fargate 的計費粒度還有一個常被忽略的面向：task 規格只能從 AWS 預定義的 vCPU/memory 組合中選。如果應用只需要 0.3 vCPU / 512 MB，最小可選的配置是 0.25 vCPU / 0.5 GB，但如果需要 0.3 vCPU / 1 GB，就得選 0.5 vCPU / 1 GB — 多付了 0.2 vCPU 的費用。這個「階梯式浪費」在小規格 task 上比例最高。</p>
<h2 id="fargate-vs-ec2-launch-type-的成本比較">Fargate vs EC2 launch type 的成本比較</h2>
<p>EC2 launch type 的成本結構不同：付的是 EC2 instance 的時數（不管上面跑幾個 task），加上 ECS 本身不收費。省的是 Fargate 的 markup，多的是 instance 管理（AMI 更新、capacity provider 設定、instance 閒置時仍計費）。</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>Fargate 月費</th>
          <th>EC2（t3.medium）月費</th>
          <th>差異</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>1 task, 1 vCPU / 2 GB, 持續</td>
          <td>~$45</td>
          <td>~$30（共享 instance）</td>
          <td>+50%</td>
      </tr>
      <tr>
          <td>5 tasks, 各 0.5 vCPU / 1 GB</td>
          <td>~$113</td>
          <td>~$30（1 台 t3.medium 裝得下）</td>
          <td>+277%</td>
      </tr>
      <tr>
          <td>20 tasks, 各 1 vCPU / 2 GB</td>
          <td>~$900</td>
          <td>~$240（4 台 t3.xlarge）</td>
          <td>+275%</td>
      </tr>
      <tr>
          <td>流量波動大，尖峰 10 tasks / 離峰 1</td>
          <td>~$180（加權平均）</td>
          <td>~$150（需預留尖峰容量）</td>
          <td>+20%</td>
      </tr>
  </tbody>
</table>
<p>幾個判讀要點：</p>
<ul>
<li>task 數量少且持續運行時，Fargate 的溢價比例最高（+50% 到 +277%），但絕對金額小（$15-$80/月的差距），不值得為此承擔 instance 管理的維運負擔</li>
<li>task 數量多且持續運行時，EC2 的絕對節省量開始可觀（$660/月），這時候切換的維運成本有回報</li>
<li>流量波動大時，Fargate 的優勢是按需計費 — 離峰時 task 數降下來就停止計費，EC2 instance 閒置時仍然計費。波動越大，Fargate 的成本效益越接近或超過 EC2</li>
</ul>
<h2 id="fargate-spot">Fargate Spot</h2>
<p>Fargate Spot 使用 AWS 的閒置容量，價格約為 on-demand 的 30%（折扣幅度 ~70%），代價是 AWS 可以隨時回收容量、task 會收到 SIGTERM 後被終止。</p>
<p>適用條件：task 能在 120 秒內優雅停止、應用有重試機制或上游有 load balancer 自動移除不健康的 target。批次處理、背景 worker、可中斷的佇列消費者是典型的 Spot 候選。對外直接服務的 API 通常混合部署 — 基線容量用 on-demand、彈性擴張部分用 Spot。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_ecs_service&#34; &#34;api&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">capacity_provider_strategy</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    capacity_provider</span> <span class="o">=</span> <span class="s2">&#34;FARGATE&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    weight</span>            <span class="o">=</span> <span class="m">1</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    base</span>              <span class="o">=</span> <span class="m">2</span><span class="c1">  # 至少 2 個 on-demand task 保底
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"></span>  }
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">  <span class="k">capacity_provider_strategy</span> {
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">    capacity_provider</span> <span class="o">=</span> <span class="s2">&#34;FARGATE_SPOT&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">    weight</span>            <span class="o">=</span> <span class="m">3</span><span class="c1">  # 擴張時 3/4 的 task 用 Spot
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>  }
</span></span><span class="line"><span class="ln">14</span><span class="cl">}</span></span></code></pre></div><p><code>base = 2</code> 確保至少有兩個 on-demand task 在線（不會被回收），<code>weight</code> 比例讓後續擴張的 task 優先使用 Spot。中斷發生時 ECS 會自動在 on-demand 上補充，但補充需要時間（task 啟動 + health check 通過），這段期間服務容量會短暫下降。</p>
<h2 id="compute-savings-plans">Compute Savings Plans</h2>
<p>Compute Savings Plans 是對 Fargate（和 EC2、Lambda）的預付承諾折扣：承諾每小時固定消費 X 美元的運算量，換取 1 年或 3 年的折扣（1 年約 -20%、3 年約 -40%，視具體方案）。</p>
<p>關鍵判斷：承諾量（$/hr）設在實際用量的多少比例。保守做法是設在過去 3 個月最低用量的 80% — 這部分幾乎確定會用到，享受折扣；超過承諾量的部分自動按 on-demand 計費，不會浪費。</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"><span class="c1"># 查過去 90 天的 Fargate 用量趨勢</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws ce get-cost-and-usage <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --time-period <span class="nv">Start</span><span class="o">=</span>2026-03-01,End<span class="o">=</span>2026-06-01 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --granularity MONTHLY <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --metrics <span class="s2">&#34;UnblendedCost&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;{&#34;Dimensions&#34;:{&#34;Key&#34;:&#34;SERVICE&#34;,&#34;Values&#34;:[&#34;Amazon Elastic Container Service&#34;]}}&#39;</span></span></span></code></pre></div><p>Savings Plans 跟 Fargate Spot 可以疊加：Spot task 的費用也能用 Savings Plans 折抵。先用 Savings Plans 降低基線成本，再用 Spot 降低彈性擴張的成本，兩層折扣疊起來可以把 Fargate 的實際單價壓到接近 EC2 on-demand。</p>
<h2 id="task-規格的-rightsizing">Task 規格的 rightsizing</h2>
<p>Fargate task 的 vCPU 和記憶體配置如果設得過大，多出來的資源每小時都在計費。rightsizing 的目標是讓 task 規格貼合實際使用量，但留足安全餘裕。</p>
<h3 id="量測實際使用量">量測實際使用量</h3>
<p>開啟 CloudWatch Container Insights 後，每個 task 的 CPU 和記憶體使用量會自動上報。觀察 7-14 天的 p95 值：</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"><span class="c1"># 查 ECS service 過去 7 天的 CPU p95</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws cloudwatch get-metric-statistics <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --namespace ECS/ContainerInsights <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --metric-name CpuUtilized <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --dimensions <span class="nv">Name</span><span class="o">=</span>ServiceName,Value<span class="o">=</span>api <span class="nv">Name</span><span class="o">=</span>ClusterName,Value<span class="o">=</span>prod <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --start-time 2026-06-19T00:00:00Z <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --end-time 2026-06-26T00:00:00Z <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --period <span class="m">3600</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --statistics p95</span></span></code></pre></div><h3 id="判斷調整方向">判斷調整方向</h3>
<table>
  <thead>
      <tr>
          <th>p95 使用率</th>
          <th>判斷</th>
          <th>動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CPU &lt; 30%</td>
          <td>過度配置，浪費明顯</td>
          <td>降一級 vCPU</td>
      </tr>
      <tr>
          <td>CPU 30-70%</td>
          <td>合理範圍，有足夠餘裕應對尖峰</td>
          <td>維持</td>
      </tr>
      <tr>
          <td>CPU &gt; 80%</td>
          <td>餘裕不足，尖峰時可能觸發 throttling</td>
          <td>升一級 vCPU 或增加 task 數</td>
      </tr>
      <tr>
          <td>Memory &lt; 40%</td>
          <td>過度配置</td>
          <td>降一級 memory</td>
      </tr>
      <tr>
          <td>Memory &gt; 80%</td>
          <td>OOM kill 風險</td>
          <td>升一級 memory</td>
      </tr>
  </tbody>
</table>
<p>調整後觀察 3-5 天確認沒有效能退化再進入下一輪。每次只調一個維度（CPU 或 memory），避免同時改兩個變數無法歸因。</p>
<h3 id="fargate-可選的規格組合">Fargate 可選的規格組合</h3>
<p>Fargate 的 vCPU 和 memory 不能任意搭配。常用的組合：</p>
<table>
  <thead>
      <tr>
          <th>vCPU</th>
          <th>可選 Memory 範圍</th>
          <th>典型用途</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>0.25</td>
          <td>0.5 / 1 / 2 GB</td>
          <td>輕量 sidecar、cron job</td>
      </tr>
      <tr>
          <td>0.5</td>
          <td>1 / 2 / 3 / 4 GB</td>
          <td>小型 API、worker</td>
      </tr>
      <tr>
          <td>1</td>
          <td>2 / 3 / 4 / 5 / 6 / 7 / 8 GB</td>
          <td>標準 API、中型 worker</td>
      </tr>
      <tr>
          <td>2</td>
          <td>4 ~ 16 GB</td>
          <td>高負載 API、批次處理</td>
      </tr>
      <tr>
          <td>4</td>
          <td>8 ~ 30 GB</td>
          <td>資料處理、ML inference</td>
      </tr>
  </tbody>
</table>
<p>選的時候從最小的「能跑」組合開始，用 Container Insights 量測後再調。常見的浪費是把所有 task 都設成 1 vCPU / 2 GB — 一個只用 0.1 vCPU / 256 MB 的 sidecar 也配了同樣的規格。</p>
<h2 id="何時從-fargate-切到-ec2">何時從 Fargate 切到 EC2</h2>
<p>切換的判斷不只看成本差額，還要看維運能力。EC2 launch type 需要管理：AMI 更新（安全 patch）、instance draining（rolling update 時把 task 遷走再關 instance）、capacity provider 的擴縮邏輯、instance 的 security group 與 IAM role。</p>
<table>
  <thead>
      <tr>
          <th>判斷維度</th>
          <th>留在 Fargate</th>
          <th>切到 EC2</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月費差額</td>
          <td>&lt; $200</td>
          <td>&gt; $500 且持續 3 個月</td>
      </tr>
      <tr>
          <td>團隊維運能力</td>
          <td>沒有專人管 instance</td>
          <td>有平台工程師或 DevOps</td>
      </tr>
      <tr>
          <td>流量型態</td>
          <td>波動大、有明顯離峰</td>
          <td>穩定、24/7 持續運行</td>
      </tr>
      <tr>
          <td>GPU 需求</td>
          <td>不需要</td>
          <td>需要（Fargate 不支援 GPU）</td>
      </tr>
      <tr>
          <td>啟動速度</td>
          <td>可接受 cold start</td>
          <td>需要 &lt;1s 啟動（EC2 instance 已在線）</td>
      </tr>
  </tbody>
</table>
<p>混合部署是常見的中間路線：基線容量用 EC2（成本低、啟動快），尖峰彈性用 Fargate Spot（按需、不需預留）。這需要同時維護兩種 capacity provider，複雜度較高。</p>
<h2 id="成本監控">成本監控</h2>
<p>把 ECS 的成本歸因到服務層級需要兩個機制：task 層的 tag propagation 和 Cost Explorer 的 tag 維度。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_ecs_service&#34; &#34;api&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"></span><span class="n">  propagate_tags</span> <span class="o">=</span> <span class="s2">&#34;SERVICE&#34;</span>
</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"><span class="n">  tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">    service</span>     <span class="o">=</span> <span class="s2">&#34;payment-api&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">    env</span>         <span class="o">=</span> <span class="s2">&#34;prod&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">    cost-center</span> <span class="o">=</span> <span class="s2">&#34;cc-payments&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  }
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p><code>propagate_tags = &quot;SERVICE&quot;</code> 讓 service 的 tag 自動傳播到每個 task，Cost Explorer 就能按 <code>service</code> 或 <code>cost-center</code> 維度拆分 Fargate 費用。這跟<a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>的 tagging 規範對齊 — tag 是成本可見性的地基。</p>
<p>定期（月初或月中）檢查 Cost Explorer 的 Fargate 費用趨勢：</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">aws ce get-cost-and-usage <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --time-period <span class="nv">Start</span><span class="o">=</span>2026-06-01,End<span class="o">=</span>2026-06-26 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --granularity DAILY <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --metrics <span class="s2">&#34;UnblendedCost&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --group-by <span class="nv">Type</span><span class="o">=</span>TAG,Key<span class="o">=</span>service <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --filter <span class="s1">&#39;{&#34;Dimensions&#34;:{&#34;Key&#34;:&#34;SERVICE&#34;,&#34;Values&#34;:[&#34;Amazon Elastic Container Service&#34;]}}&#39;</span></span></span></code></pre></div><p>費用突然跳升時，先看是 task 數增加（auto-scaling 觸發）還是單價變化（Savings Plans 過期或 Spot 中斷後自動回補為 on-demand）。這兩者的處理方式不同：前者檢查 scaling policy、後者檢查 Savings Plans 到期日和 Spot 回收頻率。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/infra/05-core-services/compute-ecs-eks/" data-link-title="運算平台上 IaC — ECS 與 EKS" data-link-desc="容器運算平台的 IaC 描述：ECS 與 EKS 選型、task definition 與映像版本解耦、IAM task role 分離、auto-scaling 策略">運算平台上 IaC</a>：ECS vs EKS 選型、Fargate 的定位</li>
<li>→ <a href="/blog/infra/08-governance-habits/" data-link-title="模組八：治理好習慣 — 規模長大後不失控的最小節奏" data-link-desc="tagging 規範、secrets 不進 code、成本可見性、最小可行節奏，規模長大後不失控">模組八：治理好習慣</a>：tagging 與成本可見性的地基</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：運行期的 RI / Spot / rightsizing 策略</li>
</ul>
]]></content:encoded></item><item><title>9.7 成本邊界與 efficiency</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/</guid><description>&lt;h2 id="概念定位">概念定位&lt;/h2>
&lt;p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 &lt;em>在每一個容量決策點&lt;/em> 把「多保險」跟「多省錢」一起評估。&lt;/p>
&lt;p>跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型&lt;/a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。&lt;/p>
&lt;p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。&lt;/p>
&lt;h2 id="cost-per-request-模型">Cost per request 模型&lt;/h2>
&lt;p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。&lt;/p>
&lt;p>&lt;strong>基本公式&lt;/strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
&lt;strong>分 stage 拆解&lt;/strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
&lt;strong>分 endpoint 拆解&lt;/strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。&lt;/p>
&lt;p>&lt;strong>對齊業務 metric&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>cost per active user：總成本 / MAU&lt;/li>
&lt;li>cost per transaction：總成本 / 完成的訂單數&lt;/li>
&lt;li>cost per ML inference：總成本 / inference 次數&lt;/li>
&lt;/ul>
&lt;p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。&lt;/p>
&lt;p>對應案例：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降&lt;/a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &amp;#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降&lt;/a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。&lt;/p>
&lt;p>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片&lt;/a>。&lt;/p>
&lt;h2 id="cost-curve-形狀">Cost curve 形狀&lt;/h2>
&lt;p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。&lt;/p>
&lt;p>&lt;strong>On-demand（pay-per-use）&lt;/strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
&lt;strong>Reserved instances（RI）/ Savings Plans&lt;/strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
&lt;strong>Spot instances&lt;/strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。&lt;/p></description><content:encoded><![CDATA[<h2 id="概念定位">概念定位</h2>
<p>成本工程的責任是讓容量決策有經濟邊界。沒有成本意識時、容量規劃會「保險起見全部擴」、最終帳單炸裂；有成本意識之後、能 <em>在每一個容量決策點</em> 把「多保險」跟「多省錢」一起評估。</p>
<p>跟 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> 的關係：9.6 算「該訂多少容量」、9.7 算「這樣訂值不值得」。兩者必須一起做、不能先決定容量再算成本。</p>
<p>本章從 cost per request 這個 unit economics 開始、推到 cost curve、TCO、降級成本、人力成本工程化、FinOps 整合。讀完後讀者能回答「容量設計的成本邊界在哪、什麼時候該降級而非擴容」。</p>
<h2 id="cost-per-request-模型">Cost per request 模型</h2>
<p>雲端帳單從月度視角看是黑箱、從 cost per request 視角看可拆解。</p>
<p><strong>基本公式</strong>：月帳單總額 / 月總 RPS = cost per request。但這只是平均、不同 endpoint 成本差很大。
<strong>分 stage 拆解</strong>：app compute + DB read + DB write + cache + network egress + 第三方 API。每個 stage 自己有 unit cost。
<strong>分 endpoint 拆解</strong>：登入請求可能 $0.0001、結帳請求可能 $0.001（10x 差距）。原因：結帳走更多 stage、可能跨 region、可能呼叫第三方支付。</p>
<p><strong>對齊業務 metric</strong>：</p>
<ul>
<li>cost per active user：總成本 / MAU</li>
<li>cost per transaction：總成本 / 完成的訂單數</li>
<li>cost per ML inference：總成本 / inference 次數</li>
</ul>
<p>業務 metric 級別的 cost 才能跟收入對比、才能算 unit economics。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato 50% 成本下降</a> — 算出每筆計費事件的 cost per request 後、發現 TiDB over-provision 拖累、遷移 DynamoDB 後減半；<a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix Aurora 28% 成本降</a> — DB consolidation 把多套 DB 的 cost 統一到 Aurora、Aurora 自己的 cost per request 更便宜。</p>
<p>詳見 <a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request 卡片</a>。</p>
<h2 id="cost-curve-形狀">Cost curve 形狀</h2>
<p>不同 pricing 模式的 cost curve 形狀不同、組合起來才能最佳化。</p>
<p><strong>On-demand（pay-per-use）</strong>：流量上升、成本同步上升。線性 cost curve。優點：彈性、不用承諾；缺點：單位成本最貴。
<strong>Reserved instances（RI）/ Savings Plans</strong>：承諾 1-3 年用量、單位成本降 30-60%。階梯 cost curve。優點：便宜；缺點：承諾期內如果用量低、浪費。
<strong>Spot instances</strong>：用 cloud 閒置 capacity、單位成本降 70-90%。可被中斷。優點：最便宜；缺點：可能突然被收回。</p>
<p><strong>最佳組合通常是「Reserved baseline + On-demand spike + Spot batch」</strong>：</p>
<ul>
<li>Reserved 覆蓋 baseline 容量（永遠用得到）</li>
<li>On-demand 處理 peak 跟 unpredicted burst</li>
<li>Spot 跑 batch 工作（不在 critical path、可被中斷）</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">Riot Games 年省 1000 萬</a> — 從自管 Mesos 遷到 EKS、降的不只是 instance cost、是 cluster 管理人力 + ops 簡化；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom 30% 成本下降</a> — DynamoDB + EKS 取代自管、釋放 DBA 人力。</p>
<h2 id="over-provisioning-vs-under-provisioning-取捨">Over-provisioning vs under-provisioning 取捨</h2>
<p>容量決策的核心經濟學問題：訂多大容量才是最划算？</p>
<p><strong>Over-provisioning 成本</strong>：每月多付 $X 雲端費。這個數字直接看帳單。
<strong>Under-provisioning 成本</strong>：sigma 機率 × downtime × revenue per minute。這個數字更難算 — 需要 historical incident rate + downtime impact analysis。</p>
<p><strong>兩個成本平衡點 = 經濟最佳 headroom</strong>。但實務上 under-provisioning 成本不容易量化、保守做法是把 sigma 機率拉高（用 worst-case 估）、headroom 訂寬一點。</p>
<p><strong>Critical workload</strong>（金融、醫療、付款）：under-provisioning 成本極高（合約違約 + 客戶流失 + 法規）、寧可 over-provisioning 30-50%。
<strong>Non-critical workload</strong>（內部工具、分析、batch）：under-provisioning 成本低、可以更貼近 minimum capacity。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">Zomato TiDB 必須 over-provision</a> — 為了應付 spike、TiDB 必須長期 over-provision；DynamoDB on-demand 不必、pay-per-use 自然處理。</p>
<h2 id="降級的成本邊界">降級的成本邊界</h2>
<p>「降級 vs 擴容」是常見容量決策、但常被當成「技術問題」而非「成本問題」。</p>
<p><strong>降級不是免費</strong>：</p>
<ul>
<li>流失轉換：UI 顯示「系統忙碌」、用戶可能放棄</li>
<li>客訴成本：客服處理客訴的 OpEx</li>
<li>品牌損失：社群媒體負面評論、口碑下降</li>
<li>合約違約：B2B 客戶可能基於 SLA 求償</li>
</ul>
<p><strong>算「降級 vs 擴容」哪個成本低</strong>：</p>
<ul>
<li>擴容成本：peak 時段多付的 cloud 費用</li>
<li>降級成本：上述四項合計</li>
<li>哪邊低就選哪邊</li>
</ul>
<p><strong>降級觸發條件</strong>通常按負載門檻 / 成本門檻 / SLA 觸發：</p>
<ul>
<li>負載門檻：utilization &gt; 85% → 啟動降級</li>
<li>成本門檻：本月雲端費已超預算 X% → 啟動降級</li>
<li>SLA 觸發：<a href="/blog/backend/knowledge-cards/error-budget/" data-link-title="Error Budget" data-link-desc="說明 SLO 允許的失敗額度如何影響發版與可靠性投入">error budget</a> 快用完 → 啟動降級保 SLA</li>
</ul>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/niantic-pokemon-go-fifty-x-surge-gcp/" data-link-title="9.C8 Niantic Pokémon GO：在 GCP 上承載 50 倍突發流量" data-link-desc="Pokémon GO 上線時實際流量達原始預估 50 倍、Google CRE 怎麼即時補容量">Pokemon GO 50x surge</a> — surge 期間無法等比擴容、必須降級保住核心遊戲機制、犧牲附加功能。</p>
<h2 id="人力成本工程化">人力成本工程化</h2>
<p>雲端帳單是顯性成本、但 <em>人力成本</em> 是常被忽略的隱性容量成本。</p>
<p><strong>自建 vs managed 的人力成本對比</strong>：</p>
<ul>
<li>自建 Kafka / PostgreSQL / Redis：需要 DBA / SRE 持續維護 + 升級 + 故障處理</li>
<li>Managed 服務（MSK、Aurora、ElastiCache）：vendor 負責 patch、backup、failover</li>
<li>差距通常 <em>3-10 倍</em> 人力成本</li>
</ul>
<p><strong>DBA / SRE / network engineer 都是隱性容量成本</strong>：</p>
<ul>
<li>一個資深 DBA 在美國年薪 $200K+、台灣 NTD 200-400 萬</li>
<li>工程師時間是有上限的、自管系統佔的時間就是 <em>無法投入產品開發</em> 的機會成本</li>
</ul>
<p><strong>「90% 工程工時下降」是管理 ROI 的關鍵</strong>：重點是把工程資源從 <em>維持</em> 轉移到 <em>建構</em>、不是拿來吹噓技術。這條自建 vs managed 的人力成本對比、是 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a> 裡「計費隨規模成長、自建 TCO 出現交叉點」那條 tripwire 的算法側 — 選型方向在 0.22 判、成本量化在這裡做。</p>
<p>對應案例：<a href="/blog/backend/09-performance-capacity/cases/spotify-kafka-to-pubsub-migration-gcp/" data-link-title="9.C9 Spotify：從自管 Kafka 遷移到 GCP Pub/Sub 的事件交付系統" data-link-desc="Spotify 把自管 Kafka 事件系統遷移到 Google Cloud Pub/Sub、避免自管 broker 的容量規劃成本">Spotify Kafka → Pub/Sub</a> — 不是因為 Pub/Sub 便宜、是因為 Spotify 規模下自管 Kafka 的人力成本不划算；<a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 90% 工程工時降</a> — managed 路線讓電信商級新串流服務只用 5-10 個工程師 launch；<a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">Capcom DBA 釋放</a> — 把 DBA 時間從 patching 轉到遊戲品質。</p>
<h2 id="finops-跟容量規劃的整合">FinOps 跟容量規劃的整合</h2>
<p>FinOps 是 <em>財務跟工程的協作框架</em>、把成本決策從事後對帳變成事前規劃。</p>
<p><strong>Showback / chargeback</strong>：把雲端成本攤到團隊 / 服務 / feature。每個團隊看得到自己的成本、自然開始 optimize。chargeback（實際扣預算）比 showback（純展示）更有效但組織複雜度高。</p>
<p><strong>每月 cost review 變成容量 review 的一部分</strong>：</p>
<ul>
<li>對比預算 vs 實際</li>
<li>找出 top 5 cost driver</li>
<li>對比上月趨勢、看是否有 anomaly</li>
<li>跟 capacity team 一起討論 right-sizing</li>
</ul>
<p><strong>Spot diversification</strong>：spot 中斷風險可以靠 <em>多 instance type 跟多 AZ</em> 分散。例如：spot pool 同時包含 m5.large + m5a.large + m5n.large、各 AZ 都有、單一 type pool 撤回時其他 type 還在。</p>
<p><strong>Right-sizing</strong>：定期 review instance type 是否最適。常見浪費：訂太大 instance（CPU / RAM 用 30%）、過時 instance generation（用 c5 沒升到 c7）、reserved 過剩。</p>
<h2 id="反模式">反模式</h2>
<p>容量成本的常見錯誤模式：</p>
<p><strong>Autoscaling max 設無限大</strong>：流量爆衝時 autoscaler 跟著爆衝、月底帳單炸裂。max 必須訂、是 financial circuit breaker。</p>
<p><strong>全部用 on-demand、沒談 reserved / savings plan</strong>：cloud spending &gt; $10K/月 已經值得跟雲商 talk discount、savings plan 通常 30-60% off。</p>
<p><strong>沒成本 monitoring、直到帳單來才知道</strong>：要建 daily cost dashboard、anomaly 即時 alert、不要等月帳單。</p>
<p><strong>降級用人工觸發、出事時來不及</strong>：降級邏輯要 <em>自動化</em>、按 metric 觸發、不是 oncall 工程師看到 dashboard 才下指令。</p>
<p><strong>忘了人力成本</strong>：算 build vs buy 只算 cloud 費、忘了 SRE / DBA 時間、結果發現「省的 cloud 費 &lt; 多花的人力」。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>教學重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>50% 成本下降（從 over-provision 解放）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/riot-games-eks-multi-cluster/" data-link-title="9.C12 Riot Games：246 個 EKS cluster 的多遊戲多地區治理" data-link-desc="Riot Games 從 Mesos 遷移到 EKS、用 246 個 cluster 跨遊戲跨地區治理、年省 1000 萬美金">9.C12 Riot Games</a></td>
          <td>年省 1000 萬（EKS 替代 Mesos）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">9.C23 Netflix</a></td>
          <td>28% 成本下降（DB consolidation）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">9.C29 Lemino</a></td>
          <td>90% 工程工時降（managed 路線）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/capcom-gaming-dynamodb-eks/" data-link-title="9.C19 Capcom：Resident Evil / Monster Hunter 在 DynamoDB &#43; EKS 上的遊戲後端" data-link-desc="Capcom 把 Resident Evil、Street Fighter、Monster Hunter 遊戲後端跑在 DynamoDB &#43; EKS、單一秒位數延遲、營運成本降 30%">9.C19 Capcom</a></td>
          <td>30% 成本下降（DBA 釋放到遊戲品質）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a></li>
<li>下游：<a href="/blog/backend/09-performance-capacity/performance-observability/" data-link-title="9.8 效能可觀測性" data-link-desc="saturation metric、USE / RED method、cost dashboard">9.8 效能可觀測性</a>（cost attribution）</li>
<li>跨模組：<a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">04.14 cost attribution</a></li>
</ul>
<h2 id="既建知識卡片">既建知識卡片</h2>
<ul>
<li><a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">Cost Per Request</a></li>
<li><a href="/blog/backend/knowledge-cards/headroom-budget/" data-link-title="Headroom Budget" data-link-desc="說明容量規劃中為應付異常 burst &#43; AZ 故障 &#43; forecast 誤差的安全餘量">Headroom Budget</a></li>
</ul>
]]></content:encoded></item><item><title>模組八：治理好習慣 — 規模長大後不失控的最小節奏</title><link>https://tarrragon.github.io/blog/infra/08-governance-habits/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/08-governance-habits/</guid><description>&lt;p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方、給帳單欄位填個用途。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo、對著一張看不出誰花的雲帳單開跨部門會議。這一章談的就是這組「現在花幾分鐘、未來省幾天」的最小節奏。&lt;/p>
&lt;p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源數量從幾十個長到幾百個時，「這是誰的、為什麼存在、花了多少」這三個問題若沒有預先在資源上留下答案，就只能靠人腦記憶與口頭考古，而記憶會隨著人員流動蒸發。&lt;/p>
&lt;p>先界定這一章的邊界。身分與憑證本身怎麼設計 — IAM role、OIDC、最小權限 — 是模組二「身分與憑證地基」的範圍，這一章只談 secret 的儲存與引用：機密值放在哪、IaC 怎麼安全地指到它。成本這一塊也分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這一章，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 &lt;a href="https://tarrragon.github.io/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理&lt;/a> 的範圍。&lt;/p>
&lt;h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據&lt;/h2>
&lt;p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。&lt;/p>
&lt;p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Tag&lt;/th>
 &lt;th>回答的問題&lt;/th>
 &lt;th>典型值&lt;/th>
 &lt;th>缺了會怎樣&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>env&lt;/code>&lt;/td>
 &lt;td>這是哪個環境&lt;/td>
 &lt;td>&lt;code>prod&lt;/code> / &lt;code>staging&lt;/code> / &lt;code>dev&lt;/code>&lt;/td>
 &lt;td>清資源時不敢動、怕誤刪生產&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>owner&lt;/code>&lt;/td>
 &lt;td>出事找誰&lt;/td>
 &lt;td>&lt;code>team-payments&lt;/code> / &lt;code>platform&lt;/code>&lt;/td>
 &lt;td>資源孤兒化、沒人認領也沒人敢回收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>cost-center&lt;/code>&lt;/td>
 &lt;td>這筆錢算誰的&lt;/td>
 &lt;td>&lt;code>cc-1024&lt;/code> / &lt;code>growth&lt;/code>&lt;/td>
 &lt;td>帳單無法拆分、成本變成一筆沒人負責的公共支出&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>env&lt;/code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源，當每個資源都標了 &lt;code>env&lt;/code>，「列出所有 &lt;code>env=dev&lt;/code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 &lt;code>env=prod&lt;/code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。&lt;/p>
&lt;p>&lt;code>owner&lt;/code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。&lt;/p>
&lt;p>&lt;code>cost-center&lt;/code> 是成本歸屬的地基，下一節展開。&lt;/p>
&lt;p>關鍵在於 tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 &lt;code>default_tags&lt;/code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">provider&lt;/span> &lt;span class="s2">&amp;#34;aws&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> region&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;ap-northeast-1&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="k">default_tags&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> tags&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="n"> env&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;staging&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> owner&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;team-payments&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> cost-center&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;cc-1024&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> managed-by&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;terraform&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。可以用 AWS 的 tag policy 或 OPA 這類策略引擎把「缺 tag 的資源」擋在 PR 階段，讓規範變成自動護欄而不是靠人自律。&lt;/p>
&lt;h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用&lt;/h2>
&lt;p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。&lt;/p>
&lt;p>密鑰管理服務 — AWS Secrets Manager、SSM Parameter Store、HashiCorp Vault、GCP Secret Manager — 提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。&lt;/p>
&lt;p>關鍵在 IaC 怎麼引用。IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文。下面這段把 RDS 密碼從 Secrets Manager 引用進來，state 與 plan 裡出現的是 secret 的 reference，不是密碼字串：&lt;/p></description><content:encoded><![CDATA[<p>每一個治理習慣單獨看都很小：在資源上多打三個 tag、把一段連線字串挪去別的地方、給帳單欄位填個用途。但少了這些習慣，半年後的代價是另一個量級 — 翻著一頁兩百筆沒有歸屬的資源猜哪個能砍、為了輪替一把外洩的密鑰回頭 grep 整個 repo、對著一張看不出誰花的雲帳單開跨部門會議。這一章談的就是這組「現在花幾分鐘、未來省幾天」的最小節奏。</p>
<p>治理習慣的責任是讓基礎設施在規模長大後仍然可被盤點、可被追責、可被回收。資源數量從幾十個長到幾百個時，「這是誰的、為什麼存在、花了多少」這三個問題若沒有預先在資源上留下答案，就只能靠人腦記憶與口頭考古，而記憶會隨著人員流動蒸發。</p>
<p>先界定這一章的邊界。身分與憑證本身怎麼設計 — IAM role、OIDC、最小權限 — 是模組二「身分與憑證地基」的範圍，這一章只談 secret 的儲存與引用：機密值放在哪、IaC 怎麼安全地指到它。成本這一塊也分兩層：把資源歸屬到擁有者與用途的地基（tagging、chargeback 的依據）在這一章，運行期怎麼用 reserved instance、spot、rightsizing 去壓低帳單，是 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的範圍。</p>
<h2 id="tagging-規範查帳與清資源的依據">Tagging 規範：查帳與清資源的依據</h2>
<p>Tag 是貼在每個資源上的結構化標籤，承擔「讓資源能被機器查詢與分群」的責任。沒有 tag 的資源在 console 裡只剩一個隨機後綴的名字，人能勉強認得幾個，但一旦數量過百，任何「列出所有 staging 的資源」「算出 team-a 這個月花多少」的問題都無法用查詢回答，只能逐筆翻。Tag 把這些問題從人工考古變成一行 filter。</p>
<p>值得從第一天就強制的最小 tag 集合是三個維度，各自回答一個治理問題：</p>
<table>
  <thead>
      <tr>
          <th>Tag</th>
          <th>回答的問題</th>
          <th>典型值</th>
          <th>缺了會怎樣</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>env</code></td>
          <td>這是哪個環境</td>
          <td><code>prod</code> / <code>staging</code> / <code>dev</code></td>
          <td>清資源時不敢動、怕誤刪生產</td>
      </tr>
      <tr>
          <td><code>owner</code></td>
          <td>出事找誰</td>
          <td><code>team-payments</code> / <code>platform</code></td>
          <td>資源孤兒化、沒人認領也沒人敢回收</td>
      </tr>
      <tr>
          <td><code>cost-center</code></td>
          <td>這筆錢算誰的</td>
          <td><code>cc-1024</code> / <code>growth</code></td>
          <td>帳單無法拆分、成本變成一筆沒人負責的公共支出</td>
      </tr>
  </tbody>
</table>
<p><code>env</code> 是清資源時的安全護欄。回收動作最大的恐懼是誤刪生產資源，當每個資源都標了 <code>env</code>，「列出所有 <code>env=dev</code> 且 30 天無流量的資源」就是一條可以放心執行的清理查詢，而 <code>env=prod</code> 的資源自動被排除在批次刪除之外。沒有這個 tag，任何自動化清理都因為怕誤傷而不敢落地，最後退回人工逐筆確認，於是根本沒人去清。</p>
<p><code>owner</code> 解決資源孤兒化。服務出狀況、或是看到一個用途不明的資源時，第一個問題是「這誰的」。標了 owner，告警可以自動路由、清理前可以自動通知認領；沒標，這個資源就停在「沒人敢動、因為不知道砍了會不會弄壞什麼」的狀態，永久占用配額與費用。團隊命名比個人名好 — 人會離職，團隊邊界相對穩定。</p>
<p><code>cost-center</code> 是成本歸屬的地基，下一節展開。</p>
<p>關鍵在於 tag 必須在資源建立時就由 IaC 寫進去，而不是事後補。Terraform 的 <code>default_tags</code> 讓一個 provider 區塊內的所有資源自動繼承一組 tag，避免逐個資源手動標、也避免漏標：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">provider</span> <span class="s2">&#34;aws&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  region</span> <span class="o">=</span> <span class="s2">&#34;ap-northeast-1&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  <span class="k">default_tags</span> {
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    tags</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">      env</span>         <span class="o">=</span> <span class="s2">&#34;staging&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">      owner</span>       <span class="o">=</span> <span class="s2">&#34;team-payments&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">      cost-center</span> <span class="o">=</span> <span class="s2">&#34;cc-1024&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">      managed-by</span>  <span class="o">=</span> <span class="s2">&#34;terraform&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    }
</span></span><span class="line"><span class="ln">11</span><span class="cl">  }
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>事後補 tag 是個會無限拖延的工作，因為它不影響任何功能、沒有 deadline、永遠排在 backlog 最後。判讀訊號很簡單：定期跑一條「列出缺少必填 tag 的資源」的查詢，數字若持續成長，代表有人繞過 IaC 手動開資源 — 這既是 tag 問題，也是模組一「Console 唯讀」紀律鬆動的徵兆。可以用 AWS 的 tag policy 或 OPA 這類策略引擎把「缺 tag 的資源」擋在 PR 階段，讓規範變成自動護欄而不是靠人自律。</p>
<h2 id="secrets-不進-code機密值的儲存與引用">Secrets 不進 code：機密值的儲存與引用</h2>
<p>機密值 — 資料庫密碼、第三方 API key、簽章用的私鑰 — 要存在專用的密鑰管理服務裡，而 code 與 IaC 只持有指向它的參照，不持有值本身。這條規則承擔的責任是把「機密外洩的爆炸半徑」與「程式碼的散布範圍」脫鉤：一旦密碼寫進 repo，它就跟著每一次 clone、每一份 CI 快取、每一個 fork 擴散，輪替時無法保證所有副本都更新，git 歷史更是會把它永久留存，即使後來刪掉那一行。</p>
<p>密鑰管理服務 — AWS Secrets Manager、SSM Parameter Store、HashiCorp Vault、GCP Secret Manager — 提供的是一個有存取控制、有審計紀錄、可輪替的集中儲存。值放在這裡，誰讀過、什麼時候讀的都有 log，輪替時只改一個地方，所有引用方下次讀取就拿到新值。</p>
<p>關鍵在 IaC 怎麼引用。IaC 應該存的是密鑰的 ARN（或等價的資源識別碼）與「在執行期去讀它」的指令，而不是密鑰的明文。下面這段把 RDS 密碼從 Secrets Manager 引用進來，state 與 plan 裡出現的是 secret 的 reference，不是密碼字串：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">data</span> <span class="s2">&#34;aws_secretsmanager_secret&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;prod/payments/db-password&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">}
</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"><span class="k">data</span> <span class="s2">&#34;aws_secretsmanager_secret_version&#34; &#34;db&#34;</span> {
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  secret_id</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">id</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">}
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_db_instance&#34; &#34;payments&#34;</span> {<span class="c1">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">  # 引用 secret 的值、但這個值不是寫在 code 裡
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"></span><span class="n">  password</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_secretsmanager_secret_version</span><span class="p">.</span><span class="k">db</span><span class="p">.</span><span class="k">secret_string</span><span class="c1">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1">  # ...
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"></span>}</span></span></code></pre></div><p>這裡有一個常被忽略的邊界：Terraform 即使從 Secrets Manager 讀值，那個值仍然會以明文落進 state file。所以「不進 code」只是第一道，state 後端的加密與存取控制（模組一的 state 地基）是同等重要的第二道 — 否則密鑰只是從 repo 搬到了一個沒鎖好的 state bucket。判讀訊號：定期用 secret 掃描工具（gitleaks、trufflehog）掃 repo 與 CI log，任何命中都當成需要輪替的外洩事件處理，而不是刪掉那行就算了，因為 git 歷史與既有 clone 已經保不住了。</p>
<p>機密的命名也值得約定。用 <code>env/service/purpose</code> 這類有結構的路徑（如 <code>prod/payments/db-password</code>），讓存取策略可以用前綴授權 — 給某個 service 的 role 只能讀 <code>prod/payments/*</code>，自然落實最小權限。誰能讀哪些 secret 的權限設計屬於模組二，更完整的密鑰生命週期、輪替策略與資料保護在 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>。</p>
<h2 id="成本可見性每筆花費都對得到擁有者與用途">成本可見性：每筆花費都對得到擁有者與用途</h2>
<p>成本可見性的目標是讓帳單上的每一筆花費都能回答「這是誰的、為了什麼」。雲帳單預設是一筆按服務類型加總的數字 — EC2 多少、RDS 多少 — 這個視角能告訴你花在哪類資源，卻答不出花在哪個團隊、哪個產品線、哪個功能。當這個問題答不出來，成本就變成一筆沒人負責的公共支出，沒有人有動機去優化自己看不到的帳。</p>
<p>把成本拆解到擁有者的地基，正是前面的 tagging。雲廠商的成本分攤工具（AWS Cost Explorer、Cost Allocation Tags、GCP 的 billing label）能用 tag 當分群維度，前提是那些 tag 要先在 billing 後台啟用為「成本分攤標籤」。啟用後，<code>cost-center</code> 和 <code>owner</code> 就從單純的標籤升級成帳單的可查詢維度，於是「team-payments 這個月花多少」「staging 環境占總成本幾成」變成一張報表而不是一場會議。</p>
<p>可見性先於優化，這個順序不能反。看不見的成本無法被歸屬，無法歸屬就無法問責，沒有問責就沒有人去做優化。所以這一章把地基鋪好 — 資源有 tag、tag 進了 billing 維度、報表能拆到團隊 — 之後運行期那些真正省錢的手段才有施力點。判讀訊號：設一條成本異常告警（如日均花費超過基線某個百分比就通知），當告警觸發時，因為有 tag，你能立刻定位是哪個團隊的哪類資源在漲，而不是面對一個總數乾瞪眼。</p>
<p>到了「知道誰花多少、接下來怎麼省」這一步 — reserved instance 的承諾折扣、spot 的可中斷算力、閒置資源的 rightsizing 與排程關機 — 就進入 <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a> 的運行期優化範圍。這一章負責的是讓那些優化「有帳可查、有人可問」。</p>
<h2 id="最小可行節奏先把地基跑起來再逐步加">最小可行節奏：先把地基跑起來，再逐步加</h2>
<p>治理的最小可行節奏，是早期只立「拔掉就會痛、補起來很貴」的那幾條規範，其餘留到規模逼出需求時再加。治理機制本身有維護成本 — 每一條策略規則、每一個審批關卡、每一套標籤分類法都要有人維護、有人解釋、有人在它擋錯東西時來救。在團隊還小、資源還少時堆滿企業級治理框架，付出的是當下的速度，換來的是一套還用不到的複雜度。</p>
<p>判斷一條治理規範該不該現在就立，看它的「補救成本曲線」。有些規範越晚補越貴，因為它要改的是既有資源的既成事實：</p>
<ul>
<li><strong>Tagging</strong>：越晚補越貴。幾百個沒 tag 的資源要回頭逐個考古歸屬，而當初建立時順手標只要幾秒。屬於 day-1 該立。</li>
<li><strong>Secrets 不進 code</strong>：幾乎無法事後補救。一旦密鑰進了 git 歷史就回不去，只能輪替所有外洩的密鑰。屬於 day-1 鐵律。</li>
<li><strong>成本分攤維度</strong>：依賴 tagging，tag 立了它就近乎免費啟用。地基屬於早期，細緻的 chargeback 報表可以晚點做。</li>
<li><strong>細緻的審批流程 / 多層級策略引擎</strong>：補救成本低、可以隨時加。早期硬上反而拖慢交付。屬於規模逼出需求再做。</li>
</ul>
<p>這個曲線給出的節奏是：補救成本陡的（tagging、secrets）從第一天就用 IaC 強制進去，因為它們事後補的代價是逐筆考古或全面輪替；補救成本平的（複雜審批、精細策略）等到痛點真的出現 — 開始有人手滑誤刪、開始有跨團隊的權限爭議 — 再有針對性地加，那時你也才知道該往哪個方向加。</p>
<p>這個節奏跟模組零的成熟度階梯是同一套思路：基礎設施的治理跟基礎設施本身一樣，是逐級長出來的，不是一次到位設計完的。過度設計的治理框架跟過度設計的架構一樣，會在還沒帶來價值之前就先收走團隊的速度。把規範變成自動護欄的工程（PR 階段擋缺 tag、CI 掃 secret）值得早投入，因為自動化的護欄維護成本低、且越早接管越省人力 — 這部分怎麼落地在 <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">infra 模組七：infra 走 PR 流程</a> 展開。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/tagging-secrets/" data-link-title="Tagging 規範與 Secrets 不進 code" data-link-desc="tag 讓資源可盤點、可清理、可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 就該立的治理地基">Tagging 規範與 Secrets 不進 code</a></td>
          <td>tag 讓資源可盤點可歸屬；密鑰存在專用服務裡而非 code 或 state，兩者都屬於 day-1 治理地基</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/cost-visibility-rhythm/" data-link-title="成本可見性與最小可行治理節奏" data-link-desc="用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加">成本可見性與最小可行治理節奏</a></td>
          <td>用 tag 驅動的成本分攤讓帳單有人負責，以及判斷什麼治理該 day-1 就立、什麼等規模逼出來再加</td>
      </tr>
      <tr>
          <td><a href="/blog/infra/08-governance-habits/handover-design/" data-link-title="職務交接與存取撤銷設計" data-link-desc="人員異動時的存取撤銷順序、credential rotation、最小交接清單，以及讓交接成本結構性降低的 infra 設計原則">職務交接與存取撤銷設計</a></td>
          <td>人員異動時的存取撤銷清單、credential rotation、IaC 降低交接成本、最小交接清單與結構性設計</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：secret 管理的更完整討論</li>
<li>→ <a href="/blog/devops/08-cost-management/" data-link-title="模組八：成本管理" data-link-desc="雲端帳單怎麼不失控 — reserved instance、spot instance、right-sizing、成本監控告警">devops 模組八：成本管理</a>：運行期的成本控制</li>
</ul>
]]></content:encoded></item><item><title>Datadog 成本治理與 Agent 配置</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/cost-governance-agent-config/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/cost-governance-agent-config/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog&lt;/a> 的 vendor deep article，深化 overview 的成本跟 Agent 段。初次接觸 Datadog 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="定位">定位&lt;/h2>
&lt;p>Datadog 是全託管觀測平台，涵蓋 metrics、logs、traces、profiling、RUM、synthetic monitoring。託管方案的核心取捨是「零運維但成本跟用量成正比」— 用得越多付得越多，而且計價維度多（host、custom metric、log ingestion、span、indexed span），成本治理需要理解每個維度的計價模型。&lt;/p>
&lt;h2 id="計價模型概覽">計價模型概覽&lt;/h2>
&lt;p>Datadog 的主要計價維度：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>計價方式&lt;/th>
 &lt;th>常見失控來源&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Infrastructure host&lt;/td>
 &lt;td>每 host/月&lt;/td>
 &lt;td>Auto-scaling 造成 host 數量波動&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Custom metrics&lt;/td>
 &lt;td>每 unique time series/月&lt;/td>
 &lt;td>Label 爆炸（同 cardinality 問題）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log ingestion&lt;/td>
 &lt;td>每 GB ingested/月&lt;/td>
 &lt;td>Debug log level 忘記關&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Log indexed retention&lt;/td>
 &lt;td>每 million events × 天/月&lt;/td>
 &lt;td>預設 retention 太長&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>APM host + indexed span&lt;/td>
 &lt;td>每 host/月 + 每 million span&lt;/td>
 &lt;td>Sampling 沒設、全收&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Profiling&lt;/td>
 &lt;td>每 host/月（APM 加購）&lt;/td>
 &lt;td>整體成本疊加&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>多數 Datadog 成本失控的根因是 custom metrics 跟 log ingestion — 兩者跟 cardinality 跟 log volume 直接相關，成長可以很快。&lt;/p>
&lt;h2 id="custom-metrics-成本控制">Custom Metrics 成本控制&lt;/h2>
&lt;h3 id="什麼算-custom-metric">什麼算 custom metric&lt;/h3>
&lt;p>Datadog 把每個 unique 的 metric name + tag 組合算一個 time series。&lt;code>http_requests_total{service=checkout, method=GET, status=200}&lt;/code> 跟 &lt;code>http_requests_total{service=checkout, method=POST, status=500}&lt;/code> 是兩個 time series。&lt;/p>
&lt;p>Tag 的笛卡爾積決定 series 數量。5 個 service × 4 個 method × 5 個 status = 100 個 series。加一個 &lt;code>region&lt;/code> tag（3 個值）就變 300 個。加一個 &lt;code>endpoint&lt;/code> tag（50 個 normalized path）就變 15,000 個。&lt;/p>
&lt;h3 id="控制策略">控制策略&lt;/h3>
&lt;p>&lt;strong>Tag 白名單&lt;/strong>：跟 Prometheus 的 label 白名單邏輯相同。只保留有查詢價值的 tag — service、method、status_class（2xx/4xx/5xx）。移除 user_id、request_id、完整 URL。&lt;/p>
&lt;p>&lt;strong>Metrics without Limits&lt;/strong>：Datadog 的功能 — 在 ingestion 之後、query 之前過濾 tag。所有 tag 都收但只 index / 計費特定 tag。適合「收全量但只查部分維度」的場景。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog</a> 的 vendor deep article，深化 overview 的成本跟 Agent 段。初次接觸 Datadog 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>。</p></blockquote>
<h2 id="定位">定位</h2>
<p>Datadog 是全託管觀測平台，涵蓋 metrics、logs、traces、profiling、RUM、synthetic monitoring。託管方案的核心取捨是「零運維但成本跟用量成正比」— 用得越多付得越多，而且計價維度多（host、custom metric、log ingestion、span、indexed span），成本治理需要理解每個維度的計價模型。</p>
<h2 id="計價模型概覽">計價模型概覽</h2>
<p>Datadog 的主要計價維度：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>計價方式</th>
          <th>常見失控來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Infrastructure host</td>
          <td>每 host/月</td>
          <td>Auto-scaling 造成 host 數量波動</td>
      </tr>
      <tr>
          <td>Custom metrics</td>
          <td>每 unique time series/月</td>
          <td>Label 爆炸（同 cardinality 問題）</td>
      </tr>
      <tr>
          <td>Log ingestion</td>
          <td>每 GB ingested/月</td>
          <td>Debug log level 忘記關</td>
      </tr>
      <tr>
          <td>Log indexed retention</td>
          <td>每 million events × 天/月</td>
          <td>預設 retention 太長</td>
      </tr>
      <tr>
          <td>APM host + indexed span</td>
          <td>每 host/月 + 每 million span</td>
          <td>Sampling 沒設、全收</td>
      </tr>
      <tr>
          <td>Profiling</td>
          <td>每 host/月（APM 加購）</td>
          <td>整體成本疊加</td>
      </tr>
  </tbody>
</table>
<p>多數 Datadog 成本失控的根因是 custom metrics 跟 log ingestion — 兩者跟 cardinality 跟 log volume 直接相關，成長可以很快。</p>
<h2 id="custom-metrics-成本控制">Custom Metrics 成本控制</h2>
<h3 id="什麼算-custom-metric">什麼算 custom metric</h3>
<p>Datadog 把每個 unique 的 metric name + tag 組合算一個 time series。<code>http_requests_total{service=checkout, method=GET, status=200}</code> 跟 <code>http_requests_total{service=checkout, method=POST, status=500}</code> 是兩個 time series。</p>
<p>Tag 的笛卡爾積決定 series 數量。5 個 service × 4 個 method × 5 個 status = 100 個 series。加一個 <code>region</code> tag（3 個值）就變 300 個。加一個 <code>endpoint</code> tag（50 個 normalized path）就變 15,000 個。</p>
<h3 id="控制策略">控制策略</h3>
<p><strong>Tag 白名單</strong>：跟 Prometheus 的 label 白名單邏輯相同。只保留有查詢價值的 tag — service、method、status_class（2xx/4xx/5xx）。移除 user_id、request_id、完整 URL。</p>
<p><strong>Metrics without Limits</strong>：Datadog 的功能 — 在 ingestion 之後、query 之前過濾 tag。所有 tag 都收但只 index / 計費特定 tag。適合「收全量但只查部分維度」的場景。</p>
<p><strong>DogStatsD 聚合</strong>：Datadog Agent 的 DogStatsD 端在 Agent 層做 pre-aggregation，把客戶端的 per-request metric 聚合成 per-interval 的摘要。減少送到 Datadog 的 data point 數量。DogStatsD 聚合在 Agent 端執行，跟 TSDB 層的 <a href="/blog/backend/knowledge-cards/recording-rule/" data-link-title="Recording Rule" data-link-desc="說明把 query-time 聚合計算推到寫入時的 pre-aggregation 機制">recording rule</a> 是不同位置的 pre-aggregation 機制。</p>
<p><strong>Usage attribution</strong>：Datadog 的 <a href="https://docs.datadoghq.com/account_management/billing/usage_attribution/">Usage Attribution</a> 功能把 custom metric 成本拆到 service / team tag，讓團隊看到自己的 metric 成本。對應 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>。</p>
<h3 id="判讀指標">判讀指標</h3>
<p>Datadog UI 的 Metric Summary 頁面顯示每個 metric name 的 tag cardinality。定期（每月）檢查 top 20 高 cardinality metric，確認是否有意外的 tag 爆炸。</p>
<h2 id="log-ingestion-成本控制">Log Ingestion 成本控制</h2>
<h3 id="index-策略">Index 策略</h3>
<p>Datadog log 的計費分兩層：ingestion（進來就計費）跟 indexing（索引後按保留天數計費）。可以 ingest 所有 log 但只 index 部分 — 非 indexed 的 log 可以在 15 分鐘的 live tail 窗口查看，之後就看不到了（除非歸檔到 S3/GCS 做 rehydrate）。</p>
<p>可操作的分層：</p>
<ul>
<li><strong>Error / warning log</strong>：index，retention 30 天</li>
<li><strong>Info log（關鍵路徑）</strong>：index，retention 7 天</li>
<li><strong>Debug log</strong>：不 index、只 ingest（live tail 用）；或直接不送</li>
<li><strong>Access log（高量）</strong>：不 index、歸檔到 S3、需要時 rehydrate</li>
</ul>
<h3 id="exclusion-filter">Exclusion filter</h3>
<p>Datadog 的 index exclusion filter 讓特定 pattern 的 log 進入 ingestion pipeline 但跳過 index。例：health check 的 access log（<code>path:/health</code>）每秒數百筆但沒有 debug 價值，設 exclusion filter 讓它不佔 index quota。</p>
<h3 id="log-pipeline-跟-datadog-log-的對應">Log pipeline 跟 Datadog log 的對應</h3>
<p><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 telemetry pipeline</a> 的 collector 端可以在 log 送到 Datadog 之前做 filtering — 低價值 log 直接 drop、不進 Datadog ingestion（連 ingestion 費用都省）。這比 Datadog 的 exclusion filter 更節省成本（exclusion filter 仍然計 ingestion 費用）。</p>
<h2 id="agent-部署配置">Agent 部署配置</h2>
<h3 id="agent-部署模式">Agent 部署模式</h3>
<table>
  <thead>
      <tr>
          <th>模式</th>
          <th>部署位置</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Host agent</td>
          <td>每台 VM 一個 agent</td>
          <td>傳統 VM 部署</td>
      </tr>
      <tr>
          <td>DaemonSet agent</td>
          <td>K8s 每個 node 一個 agent</td>
          <td>K8s 標準部署</td>
      </tr>
      <tr>
          <td>Sidecar agent</td>
          <td>每個 pod 一個 agent</td>
          <td>需要嚴格隔離時</td>
      </tr>
      <tr>
          <td>Cluster agent</td>
          <td>K8s cluster 一個</td>
          <td>收集 cluster-level metric</td>
      </tr>
  </tbody>
</table>
<p>多數 K8s 部署用 DaemonSet + Cluster Agent 組合。DaemonSet agent 收集 node-level 跟 pod-level 的 metric / log / trace；Cluster Agent 收集 cluster-level 的 metadata 跟 event。</p>
<h3 id="agent-健康判讀">Agent 健康判讀</h3>
<p>Agent 本身需要被監控 — Agent 故障時 Datadog 看到的是「資料消失」而非「Agent 掛了」。</p>
<p>判讀指標（Agent 自帶）：</p>
<ul>
<li><code>datadog.agent.running</code>：Agent process 是否存活</li>
<li><code>datadog.agent.check_run</code>：各 integration check 是否正常</li>
<li><code>datadog.dogstatsd.packets.dropped</code>：DogStatsD buffer 滿時丟棄的封包數</li>
</ul>
<p>Agent 掛掉時 dashboard 會出現 gap（資料斷層）。如果所有 host 同時斷層、問題在 Datadog backend；如果特定 host 斷層、問題在該 host 的 Agent。</p>
<h3 id="常見-agent-故障">常見 Agent 故障</h3>
<p><strong>CPU / memory over-consumption</strong>：Agent 開太多 integration check 或 DogStatsD 收太多 custom metric。修復：減少 check 數量、調整 DogStatsD 的 aggregation interval、或升級 Agent 版本（新版通常更節省資源）。</p>
<p><strong>Log collection 延遲</strong>：Agent 的 log tail 落後，log 到達 Datadog 的延遲增加。原因通常是 log rotation 設定跟 Agent 的 tail 設定不一致，或 log 量突然爆增超過 Agent 的處理能力。</p>
<p><strong>Network connectivity</strong>：Agent 到 Datadog intake endpoint 的網路問題。Agent 會 buffer 資料並重試，但 buffer 滿（預設 100MB）後會 drop。在網路不穩的環境（edge location、受限網路），需要加大 buffer 或設定 proxy。</p>
<h2 id="跟-otel-的整合">跟 OTel 的整合</h2>
<p>Datadog 支援 OpenTelemetry — 可以用 OTel SDK instrumentation + OTel Collector，把資料送到 Datadog backend。這種模式讓 instrumentation 跟 vendor 解耦，但犧牲部分 Datadog-native 功能（例如 Watchdog anomaly detection 需要 Datadog Agent 的 metadata）。</p>
<p>整合模式的選擇跟 <a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration practice</a> 的案例分析對應 — 雙軌期的成本跟語意對齊是主要挑戰。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li><a href="/blog/backend/04-observability/vendors/datadog/" data-link-title="Datadog" data-link-desc="All-in-one SaaS 觀測平台、APM / Logs / Metrics / RUM / Security">Datadog 服務頁</a>：overview 跟日常操作</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 cardinality</a>：cardinality 治理的完整策略</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 cost attribution</a>：成本歸因的組織治理</li>
<li><a href="/blog/backend/04-observability/cases/datadog-otel-migration-practice/" data-link-title="4.C7 Datadog：OTel 相容遷移實務" data-link-desc="APM 採集從專有代理轉向 OTel 相容模式的治理案例。">4.C7 Datadog OTel migration</a>：Datadog 跟 OTel 的整合案例</li>
<li><a href="/blog/backend/04-observability/vendors/opentelemetry/" data-link-title="OpenTelemetry" data-link-desc="可觀測性開放標準、SDK 與 Collector">OpenTelemetry</a>：vendor-neutral instrumentation</li>
</ul>
]]></content:encoded></item><item><title>4.C12 Cloudflare：內部觀測平台的三層能力</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/cloudflare-internal-observability-architecture/</guid><description>&lt;p>Cloudflare 的觀測架構把 monitoring、analytics 和 forensics 拆成三層 pipeline，三層各自承擔不同的 resolution、retention 和查詢模式。規模到達每秒數十億 request、300+ edge location 時，用同一套 pipeline 處理三種能力會同時在成本跟查詢延遲上碰壁。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Cloudflare 的服務涵蓋 CDN、DNS、DDoS 防護、Workers 邊緣運算與 Zero Trust 安全。每秒處理數十億 HTTP request，分布在全球 300+ 資料中心。觀測資料量極大 — 僅 HTTP request log 每秒就產生數百 GB 未壓縮的結構化日誌。&lt;/p>
&lt;p>早期觀測用單一 pipeline 處理所有資料，隨著資料量成長，pipeline 面臨三個壓力：monitoring 需要秒級即時性但不需要全量資料；analytics 需要完整資料但可以延遲分鐘級；forensics（鑑識）需要保留原始事件但查詢頻率極低。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="資料量與成本">資料量與成本&lt;/h3>
&lt;p>每秒數十億 request 的全量日誌，即使壓縮後仍是 PB 級月儲存量。把全量資料送到集中式 log backend（無論是自建 Elasticsearch 或 SaaS Datadog）的 ingestion 成本本身就是天文數字。&lt;/p>
&lt;p>Cloudflare 公開表示過去曾用過 Kafka + Elasticsearch + Grafana 的組合，但隨著 edge 節點增加，centralized ingestion 的頻寬跟儲存成本持續超線性成長。&lt;/p>
&lt;h3 id="edge-到-core-的延遲">Edge 到 Core 的延遲&lt;/h3>
&lt;p>觀測資料從 300+ edge 節點匯聚到中心叢集，網路延遲跟 bandwidth 是物理限制。monitoring 需要秒級判斷（alert 要快觸發），但全量日誌的傳輸延遲可能是分鐘級。&lt;/p>
&lt;h3 id="查詢模式衝突">查詢模式衝突&lt;/h3>
&lt;p>on-call 值班需要的是 dashboard 上的 aggregated metrics（error rate、latency percentile、traffic volume），查詢要快、資料要即時。analytics 團隊需要的是全量日誌做 ad-hoc 查詢（某個 IP 在過去 24 小時的 request pattern），查詢可以慢、但資料要完整。forensics 需要的是單一事件的原始內容（某筆 request 的完整 header 跟 body），查詢極少但需要保留數月。&lt;/p>
&lt;p>三種查詢模式在 resolution、freshness 跟 retention 上的需求完全不同，用同一套 backend 處理會讓所有人的體驗都變差。&lt;/p>
&lt;h2 id="解法三層觀測能力">解法：三層觀測能力&lt;/h2>
&lt;h3 id="monitoringpre-aggregated-metrics--alerting">Monitoring：pre-aggregated metrics + alerting&lt;/h3>
&lt;p>edge 節點在本地做 pre-aggregation — 把每秒的 request count、error count、latency histogram 聚合成每 10 秒的 metric batch，push 到中心的 metrics backend。資料量從 PB/月壓縮到 TB/月。&lt;/p>
&lt;p>Alerting 跟 dashboard 只看聚合後的 metrics，查詢延遲在毫秒級。metrics backend 用 Prometheus-compatible 儲存，Grafana 作為查詢入口。&lt;/p>
&lt;h3 id="analyticssampled--full-fidelity-log-pipeline">Analytics：sampled + full-fidelity log pipeline&lt;/h3>
&lt;p>analytics 層接收全量日誌但做分層處理：高流量 endpoint 的日誌做 adaptive sampling（保留 1%-10%），低流量跟異常 request 保留全量。日誌送到自建的 columnar store（Cloudflare 用 ClickHouse 類的 OLAP 引擎），支援 ad-hoc 查詢。&lt;/p>
&lt;p>Retention 30-90 天，查詢延遲在秒到分鐘級。成本比 monitoring 層高但仍可控 — sampling 是關鍵的成本旋鈕。&lt;/p>
&lt;h3 id="forensics原始事件歸檔">Forensics：原始事件歸檔&lt;/h3>
&lt;p>需要完整保留的事件（安全事件、DDoS 攻擊、客戶投訴關聯的 request）寫入冷儲存（object storage）。查詢走 batch 模式（scan-based），延遲在分鐘到小時級。&lt;/p>
&lt;p>Retention 按合規需求保留 6 個月到數年。成本主要是儲存（object storage 便宜），ingestion 跟 query 成本極低。&lt;/p>
&lt;h2 id="取捨">取捨&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>單一 pipeline&lt;/th>
 &lt;th>三層拆分&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>架構複雜度&lt;/td>
 &lt;td>低（一條路走完）&lt;/td>
 &lt;td>高（三條路各自維護）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>成本可控度&lt;/td>
 &lt;td>差（全量資料走同一條路，成本隨 traffic 線性成長）&lt;/td>
 &lt;td>好（每層各自有成本旋鈕）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>查詢一致性&lt;/td>
 &lt;td>高（同一個 backend 查）&lt;/td>
 &lt;td>低（三個 backend，查詢語言可能不同）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Freshness&lt;/td>
 &lt;td>被最慢的一段拖住&lt;/td>
 &lt;td>每層獨立（monitoring 秒級、analytics 分鐘級、forensics 小時級）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Debugging 路徑&lt;/td>
 &lt;td>短（一個入口）&lt;/td>
 &lt;td>長（先看 monitoring 判斷層級、再決定進 analytics 或 forensics）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>三層拆分的最大風險是 debugging 路徑變長 — on-call 先看 dashboard 發現異常，再到 analytics 查 sampled log 找 pattern，最後到 forensics 查原始事件確認細節。如果三層之間的 correlation ID（trace ID、request ID）沒有對齊，跨層查詢會斷掉。&lt;/p></description><content:encoded><![CDATA[<p>Cloudflare 的觀測架構把 monitoring、analytics 和 forensics 拆成三層 pipeline，三層各自承擔不同的 resolution、retention 和查詢模式。規模到達每秒數十億 request、300+ edge location 時，用同一套 pipeline 處理三種能力會同時在成本跟查詢延遲上碰壁。</p>
<h2 id="業務背景">業務背景</h2>
<p>Cloudflare 的服務涵蓋 CDN、DNS、DDoS 防護、Workers 邊緣運算與 Zero Trust 安全。每秒處理數十億 HTTP request，分布在全球 300+ 資料中心。觀測資料量極大 — 僅 HTTP request log 每秒就產生數百 GB 未壓縮的結構化日誌。</p>
<p>早期觀測用單一 pipeline 處理所有資料，隨著資料量成長，pipeline 面臨三個壓力：monitoring 需要秒級即時性但不需要全量資料；analytics 需要完整資料但可以延遲分鐘級；forensics（鑑識）需要保留原始事件但查詢頻率極低。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="資料量與成本">資料量與成本</h3>
<p>每秒數十億 request 的全量日誌，即使壓縮後仍是 PB 級月儲存量。把全量資料送到集中式 log backend（無論是自建 Elasticsearch 或 SaaS Datadog）的 ingestion 成本本身就是天文數字。</p>
<p>Cloudflare 公開表示過去曾用過 Kafka + Elasticsearch + Grafana 的組合，但隨著 edge 節點增加，centralized ingestion 的頻寬跟儲存成本持續超線性成長。</p>
<h3 id="edge-到-core-的延遲">Edge 到 Core 的延遲</h3>
<p>觀測資料從 300+ edge 節點匯聚到中心叢集，網路延遲跟 bandwidth 是物理限制。monitoring 需要秒級判斷（alert 要快觸發），但全量日誌的傳輸延遲可能是分鐘級。</p>
<h3 id="查詢模式衝突">查詢模式衝突</h3>
<p>on-call 值班需要的是 dashboard 上的 aggregated metrics（error rate、latency percentile、traffic volume），查詢要快、資料要即時。analytics 團隊需要的是全量日誌做 ad-hoc 查詢（某個 IP 在過去 24 小時的 request pattern），查詢可以慢、但資料要完整。forensics 需要的是單一事件的原始內容（某筆 request 的完整 header 跟 body），查詢極少但需要保留數月。</p>
<p>三種查詢模式在 resolution、freshness 跟 retention 上的需求完全不同，用同一套 backend 處理會讓所有人的體驗都變差。</p>
<h2 id="解法三層觀測能力">解法：三層觀測能力</h2>
<h3 id="monitoringpre-aggregated-metrics--alerting">Monitoring：pre-aggregated metrics + alerting</h3>
<p>edge 節點在本地做 pre-aggregation — 把每秒的 request count、error count、latency histogram 聚合成每 10 秒的 metric batch，push 到中心的 metrics backend。資料量從 PB/月壓縮到 TB/月。</p>
<p>Alerting 跟 dashboard 只看聚合後的 metrics，查詢延遲在毫秒級。metrics backend 用 Prometheus-compatible 儲存，Grafana 作為查詢入口。</p>
<h3 id="analyticssampled--full-fidelity-log-pipeline">Analytics：sampled + full-fidelity log pipeline</h3>
<p>analytics 層接收全量日誌但做分層處理：高流量 endpoint 的日誌做 adaptive sampling（保留 1%-10%），低流量跟異常 request 保留全量。日誌送到自建的 columnar store（Cloudflare 用 ClickHouse 類的 OLAP 引擎），支援 ad-hoc 查詢。</p>
<p>Retention 30-90 天，查詢延遲在秒到分鐘級。成本比 monitoring 層高但仍可控 — sampling 是關鍵的成本旋鈕。</p>
<h3 id="forensics原始事件歸檔">Forensics：原始事件歸檔</h3>
<p>需要完整保留的事件（安全事件、DDoS 攻擊、客戶投訴關聯的 request）寫入冷儲存（object storage）。查詢走 batch 模式（scan-based），延遲在分鐘到小時級。</p>
<p>Retention 按合規需求保留 6 個月到數年。成本主要是儲存（object storage 便宜），ingestion 跟 query 成本極低。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>單一 pipeline</th>
          <th>三層拆分</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>架構複雜度</td>
          <td>低（一條路走完）</td>
          <td>高（三條路各自維護）</td>
      </tr>
      <tr>
          <td>成本可控度</td>
          <td>差（全量資料走同一條路，成本隨 traffic 線性成長）</td>
          <td>好（每層各自有成本旋鈕）</td>
      </tr>
      <tr>
          <td>查詢一致性</td>
          <td>高（同一個 backend 查）</td>
          <td>低（三個 backend，查詢語言可能不同）</td>
      </tr>
      <tr>
          <td>Freshness</td>
          <td>被最慢的一段拖住</td>
          <td>每層獨立（monitoring 秒級、analytics 分鐘級、forensics 小時級）</td>
      </tr>
      <tr>
          <td>Debugging 路徑</td>
          <td>短（一個入口）</td>
          <td>長（先看 monitoring 判斷層級、再決定進 analytics 或 forensics）</td>
      </tr>
  </tbody>
</table>
<p>三層拆分的最大風險是 debugging 路徑變長 — on-call 先看 dashboard 發現異常，再到 analytics 查 sampled log 找 pattern，最後到 forensics 查原始事件確認細節。如果三層之間的 correlation ID（trace ID、request ID）沒有對齊，跨層查詢會斷掉。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/log-schema/" data-link-title="4.1 log schema 與搜尋規劃" data-link-desc="整理 log 欄位、索引與搜尋策略">4.1 Log Schema</a>：三層共用的欄位設計（correlation ID、timestamp、service tag）是 log schema 的規模化實例。</li>
<li><a href="/blog/backend/04-observability/tracing-context/" data-link-title="4.3 tracing 與 context link" data-link-desc="整理 trace id、span 與跨服務 context propagation">4.3 Tracing Context</a>：跨層 correlation 依賴 trace context propagation，edge → core 的 context 傳遞是挑戰。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：三層拆分就是 pipeline 的 routing 跟 processing 層設計。</li>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：三層各自的成本旋鈕（sampling rate、retention、storage tier）是成本歸因的實作入口。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>觀測平台帳單主要被全量日誌 ingestion 佔據，但 90% 的日誌沒人查過</li>
<li>Dashboard 查詢越來越慢，因為查詢打的是存了全量資料的同一個 backend</li>
<li>on-call 跟 analytics 團隊對觀測 backend 的需求衝突（一個要快、一個要全）</li>
<li>edge / CDN / 多 region 架構下，central pipeline 的 ingestion bandwidth 成為瓶頸</li>
<li>安全團隊要求保留原始事件 6 個月以上，但 hot tier 儲存成本撐不住</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/vision-for-observability/">Our Vision for Observability at Cloudflare</a></li>
<li><a href="https://blog.cloudflare.com/building-cloudflare-on-cloudflare/">Building Cloudflare on Cloudflare</a></li>
</ul>
]]></content:encoded></item><item><title>AWS SQS：Visibility timeout、long polling 與 Lambda event source 的成本與失敗形狀</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/visibility-polling-lambda-cost/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS&lt;/a> overview 的 implementation-layer deep article。本文的 CLI 指令語法經 LocalStack round-trip 驗證、真實 AWS 的 scaling 行為、Lambda event source mapping 細節與計費數字依 AWS 官方文件。&lt;/p>&lt;/blockquote>
&lt;h2 id="sqs-沒有-broker-ackdelivery-控制全靠-visibility-timeout">SQS 沒有 broker ACK，delivery 控制全靠 visibility timeout&lt;/h2>
&lt;p>SQS 跟自管 broker（RabbitMQ / Kafka）最大的操作差異是：consumer 不會跟 broker 維持一條長連線、也沒有 channel-level 的 ack / nack 協議。SQS 的整個 delivery 保證建立在一個計時器上 — visibility timeout。訊息被 &lt;code>ReceiveMessage&lt;/code> 拉走後進入 in-flight 狀態、在 timeout 視窗內對其他 consumer 不可見；consumer 處理成功就呼叫 &lt;code>DeleteMessage&lt;/code> 把它移除、處理失敗或當機則什麼都不做、等 timeout 到期訊息自動回到 queue 重新可見。&lt;/p>
&lt;p>這個設計把「確認處理完成」的責任從 broker 連線狀態轉移到 consumer 的主動刪除。好處是 consumer 可以隨時死掉、重啟、水平擴縮、不需要維持任何 session 狀態 — 訊息不會因為連線斷掉而遺失。代價是 visibility timeout 這個數字變成最容易設錯、後果最隱蔽的參數：設太短訊息會在 consumer 還在處理時就重新可見、被另一個 consumer 重複領走；設太長則 consumer 當機後訊息要等很久才回到 queue、retry 延遲拉長。&lt;/p>
&lt;p>實機建立一個 queue 並查 default、可以確認這個視窗的起點。新建 queue 的 &lt;code>VisibilityTimeout&lt;/code> 預設 30 秒：&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">&lt;span class="c1"># 不帶任何 attribute 建 queue&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws sqs create-queue --queue-name demo-default
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 查 default visibility timeout&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">aws sqs get-queue-attributes &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-url &amp;lt;url&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attribute-names VisibilityTimeout
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># =&amp;gt; &amp;#34;VisibilityTimeout&amp;#34;: &amp;#34;30&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>30 秒對「處理時間穩定在數百毫秒」的 task 綽綽有餘、對「呼叫第三方 API、跑批次轉檔、寫多個下游」的 task 則經常不夠。下一節先把這個參數設對，後面的故障演練再展開它設錯時的具體徵兆。&lt;/p>
&lt;h2 id="對齊-visibility-timeout-與-consumer-處理時間">對齊 visibility timeout 與 consumer 處理時間&lt;/h2>
&lt;p>設定 visibility timeout 的判準是「略高於 consumer 處理單則訊息的最大時間」、不是平均時間。Capital One 的官方 tech blog 在講 SQS + Lambda 時明示這條原則：visibility timeout 應比最大處理時間略高 — 因為決定 redelivery 的是尾端那幾則最慢的訊息、不是中位數。處理時間 p50 是 2 秒、p99 是 25 秒時、visibility timeout 要對齊 p99 加緩衝、設到 30-40 秒、而不是看 p50 設 10 秒。&lt;/p>
&lt;p>建 queue 時直接帶 &lt;code>VisibilityTimeout&lt;/code> attribute，或對既有 queue 用 &lt;code>set-queue-attributes&lt;/code> 調整：&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">&lt;span class="c1"># 建立時指定（單位：秒；上限 12 小時 = 43200）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws sqs create-queue &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-name demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attributes &lt;span class="nv">VisibilityTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">60&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># 對既有 queue 調整&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">aws sqs set-queue-attributes &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --queue-url &amp;lt;url&amp;gt; &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --attributes &lt;span class="nv">VisibilityTimeout&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="m">120&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>處理時間本身不可預測的場景（例如轉檔大小差異大、下游 API 偶發慢）、用一個固定的 queue-level visibility timeout 會兩頭不討好：對齊最壞情況會讓正常訊息當機後 retry 太慢、對齊正常情況會讓慢訊息 redelivery。SQS 給的工具是 &lt;code>ChangeMessageVisibility&lt;/code> — consumer 在處理過程中發現這則會花更久時，主動延長這一則訊息的 visibility timeout，而不影響 queue default：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">AWS SQS</a> overview 的 implementation-layer deep article。本文的 CLI 指令語法經 LocalStack round-trip 驗證、真實 AWS 的 scaling 行為、Lambda event source mapping 細節與計費數字依 AWS 官方文件。</p></blockquote>
<h2 id="sqs-沒有-broker-ackdelivery-控制全靠-visibility-timeout">SQS 沒有 broker ACK，delivery 控制全靠 visibility timeout</h2>
<p>SQS 跟自管 broker（RabbitMQ / Kafka）最大的操作差異是：consumer 不會跟 broker 維持一條長連線、也沒有 channel-level 的 ack / nack 協議。SQS 的整個 delivery 保證建立在一個計時器上 — visibility timeout。訊息被 <code>ReceiveMessage</code> 拉走後進入 in-flight 狀態、在 timeout 視窗內對其他 consumer 不可見；consumer 處理成功就呼叫 <code>DeleteMessage</code> 把它移除、處理失敗或當機則什麼都不做、等 timeout 到期訊息自動回到 queue 重新可見。</p>
<p>這個設計把「確認處理完成」的責任從 broker 連線狀態轉移到 consumer 的主動刪除。好處是 consumer 可以隨時死掉、重啟、水平擴縮、不需要維持任何 session 狀態 — 訊息不會因為連線斷掉而遺失。代價是 visibility timeout 這個數字變成最容易設錯、後果最隱蔽的參數：設太短訊息會在 consumer 還在處理時就重新可見、被另一個 consumer 重複領走；設太長則 consumer 當機後訊息要等很久才回到 queue、retry 延遲拉長。</p>
<p>實機建立一個 queue 並查 default、可以確認這個視窗的起點。新建 queue 的 <code>VisibilityTimeout</code> 預設 30 秒：</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"><span class="c1"># 不帶任何 attribute 建 queue</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs create-queue --queue-name demo-default
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 查 default visibility timeout</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="se"></span>  --attribute-names VisibilityTimeout
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># =&gt; &#34;VisibilityTimeout&#34;: &#34;30&#34;</span></span></span></code></pre></div><p>30 秒對「處理時間穩定在數百毫秒」的 task 綽綽有餘、對「呼叫第三方 API、跑批次轉檔、寫多個下游」的 task 則經常不夠。下一節先把這個參數設對，後面的故障演練再展開它設錯時的具體徵兆。</p>
<h2 id="對齊-visibility-timeout-與-consumer-處理時間">對齊 visibility timeout 與 consumer 處理時間</h2>
<p>設定 visibility timeout 的判準是「略高於 consumer 處理單則訊息的最大時間」、不是平均時間。Capital One 的官方 tech blog 在講 SQS + Lambda 時明示這條原則：visibility timeout 應比最大處理時間略高 — 因為決定 redelivery 的是尾端那幾則最慢的訊息、不是中位數。處理時間 p50 是 2 秒、p99 是 25 秒時、visibility timeout 要對齊 p99 加緩衝、設到 30-40 秒、而不是看 p50 設 10 秒。</p>
<p>建 queue 時直接帶 <code>VisibilityTimeout</code> attribute，或對既有 queue 用 <code>set-queue-attributes</code> 調整：</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"><span class="c1"># 建立時指定（單位：秒；上限 12 小時 = 43200）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs create-queue <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-name demo <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --attributes <span class="nv">VisibilityTimeout</span><span class="o">=</span><span class="m">60</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 對既有 queue 調整</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">aws sqs set-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --attributes <span class="nv">VisibilityTimeout</span><span class="o">=</span><span class="m">120</span></span></span></code></pre></div><p>處理時間本身不可預測的場景（例如轉檔大小差異大、下游 API 偶發慢）、用一個固定的 queue-level visibility timeout 會兩頭不討好：對齊最壞情況會讓正常訊息當機後 retry 太慢、對齊正常情況會讓慢訊息 redelivery。SQS 給的工具是 <code>ChangeMessageVisibility</code> — consumer 在處理過程中發現這則會花更久時，主動延長這一則訊息的 visibility timeout，而不影響 queue default：</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"><span class="c1"># consumer 拿到 ReceiptHandle 後，動態把這則延長到 120 秒</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs change-message-visibility <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --receipt-handle &lt;receipt-handle&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --visibility-timeout <span class="m">120</span></span></span></code></pre></div><p>實務上長任務 consumer 的常見寫法是「heartbeat extension」：每處理一段就呼叫一次 <code>ChangeMessageVisibility</code> 往後推、形成一個續命迴圈、直到處理完成才 <code>DeleteMessage</code>。這把「我還活著、還在處理這則」的訊號明確化、避免用一個保守的 queue-level 大數字一刀切。<code>ReceiptHandle</code> 是每次 <code>ReceiveMessage</code> 回傳的一次性 token、不是 message id — 同一則訊息被重新領取後 ReceiptHandle 會變、延長操作必須用當次領取拿到的那一個。</p>
<h2 id="long-polling-決定空輪詢成本short-polling-是預設陷阱">Long polling 決定空輪詢成本，short polling 是預設陷阱</h2>
<p>Polling 模式直接決定 SQS 的 request 帳單，因為 SQS 按 request 數計費、而 <code>ReceiveMessage</code> 即使沒拿到訊息也算一次 request。Short polling（預設、<code>WaitTimeSeconds=0</code>）的行為是「立即回應」：consumer 發 <code>ReceiveMessage</code>、SQS 抽樣一部分 server 立刻回、queue 空的時候回一個空 response。Consumer 為了即時拿到訊息會緊接著再發一次、形成高頻空輪詢 — 在低流量 queue 上、絕大多數 request 都是空回、帳單全花在「問有沒有訊息」上。</p>
<p>Long polling（<code>WaitTimeSeconds</code> 設 1-20 秒）改變這個行為：SQS 收到 <code>ReceiveMessage</code> 後、若 queue 當下沒訊息、會 hold 住這條連線最多 <code>WaitTimeSeconds</code> 秒、期間一有訊息到達就立刻回傳、整段時間都沒訊息才回空。對 consumer 端來說一個 20 秒的 long poll 取代了 20 秒內可能發出的數十次 short poll、空 request 數量大幅下降。</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"><span class="c1"># long polling：等到有訊息或最多 20 秒才回</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs receive-message <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --wait-time-seconds <span class="m">20</span></span></span></code></pre></div><p>設定 long polling 有兩個位置：per-request 帶 <code>--wait-time-seconds</code>、或 queue-level 設 <code>ReceiveMessageWaitTimeSeconds</code> attribute 讓所有 receive 預設走 long polling。後者更穩、不依賴每個 consumer 都記得帶參數。20 秒幾乎總是對的選擇：它把空輪詢壓到最低、而 latency 代價只在「queue 剛好空、訊息在 poll 結束後才到」這個邊界出現 — 大多數有持續流量的 queue 根本碰不到 20 秒上限。唯一要留意的是 consumer 的 socket timeout 必須大於 <code>WaitTimeSeconds</code>、否則 client 會在 SQS 還在 hold 連線時自己先 timeout 斷線。</p>
<h2 id="sqs--lambdaevent-source-mapping-把-polling-交給-aws">SQS + Lambda：event source mapping 把 polling 交給 AWS</h2>
<p>把 SQS 接上 Lambda 時、polling 這件事整個從應用程式碼消失、改由 Lambda 的 event source mapping 接管。Event source mapping 是 Lambda service 內部一組 managed poller、持續對 queue 做 long polling、把拉到的訊息打包成 batch 同步 invoke 函式、函式正常返回就由 service 代為 <code>DeleteMessage</code>。Consumer 端不再寫 receive / delete 迴圈、只寫處理單一 batch 的 handler。</p>
<p>這套 managed poller 的 scaling 不是線性的、有 ramp-up 上限。Capital One 觀察到的行為是：Lambda 初始開 5 個並行的 long polling 連線、隨 queue 累積每分鐘最多增加 60 個 instance、standard queue 的並行 batch 上限到 1000。這意味著 queue 突然湧入大量訊息時、Lambda 不會瞬間炸開到滿並行、而是分鐘級爬升 — 容量規劃時要把這段 ramp-up 期算進 backlog 消化時間、不能假設「訊息一到就有足夠 consumer」。</p>
<p>兩個核心參數決定每次 invoke 的形狀：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>作用</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Batch size</td>
          <td>一次 invoke 最多打包幾則訊息（standard 上限 10000、FIFO 上限 10）</td>
          <td>大 batch 省 invoke 數與成本、但放大「部分失敗整批重投」風險</td>
      </tr>
      <tr>
          <td>Batch window</td>
          <td>累積訊息的最長等待時間（<code>MaximumBatchingWindowInSeconds</code>、0-300 秒）</td>
          <td>拉長視窗讓 batch 更滿、代價是 latency；流量稀疏時尤其明顯</td>
      </tr>
  </tbody>
</table>
<p>Batch size 拉大表面上省錢 — invoke 次數少、每則訊息分攤的 request 成本低。但它跟下一節的部分失敗處理直接耦合：batch 越大、一則毒訊息拖累整批重投的範圍越大。Batch window 則是流量稀疏時讓 batch 攢滿的手段、流量本來就密集時設不設都差不多、反而會引入不必要的 latency。</p>
<h2 id="dlq-與-redrive-policy用-maxreceivecount-隔離毒訊息">DLQ 與 redrive policy：用 maxReceiveCount 隔離毒訊息</h2>
<p>毒訊息（永遠處理失敗的訊息 — 格式損壞、引用了已刪除的資源、觸發 consumer 確定性 bug）會在 visibility timeout 機制下無限重投：處理失敗、timeout 到期、重新可見、再次被領取、再次失敗。沒有上限的話這則訊息會永遠佔用 consumer 資源、且其他正常訊息的處理被它反覆插隊。Dead-letter queue（DLQ）加 <code>maxReceiveCount</code> 是 SQS 對這個問題的標準解 — 訊息被接收超過 N 次後、SQS 自動把它移到另一個指定的 queue（DLQ）、主 queue 不再被它卡住。</p>
<p>設定分兩步：先建一個普通 queue 當 DLQ、取它的 ARN、再對主 queue 設 redrive policy 指向這個 ARN 並設 <code>maxReceiveCount</code>：</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"><span class="c1"># 1. 建 DLQ 並取得 ARN</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws sqs create-queue --queue-name demo-dlq
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --queue-url &lt;dlq-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --attribute-names QueueArn
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># =&gt; &#34;QueueArn&#34;: &#34;arn:aws:sqs:us-east-1:000000000000:demo-dlq&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 2. 對主 queue 設 redrive policy（被接收 5 次後送 DLQ）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">aws sqs set-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --queue-url &lt;main-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --attributes <span class="s1">&#39;{&#34;RedrivePolicy&#34;:&#34;{\&#34;deadLetterTargetArn\&#34;:\&#34;arn:aws:sqs:us-east-1:000000000000:demo-dlq\&#34;,\&#34;maxReceiveCount\&#34;:\&#34;5\&#34;}&#34;}&#39;</span></span></span></code></pre></div><p>DLQ 不是訊息的墳場、是待診斷的暫存區。對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a> 的思路、DLQ 累積要分兩種根因處理：訊息格式錯（永遠失敗、需要修 producer 或人工丟棄）vs 下游服務暫時 down（訊息本身沒問題、修好下游後可以重放）。後者用 redrive 把訊息從 DLQ 批次放回主 queue 重新處理、對應 <a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">dlq drain</a> 的排空流程。判斷之前先看 DLQ 裡訊息的內容、不要不加判斷地 redrive — 把毒訊息 redrive 回去只會再走一輪 maxReceiveCount 又回到 DLQ。</p>
<p><code>maxReceiveCount</code> 設多少是取捨：太小（例如 1-2）會讓「下游短暫抖動」這種暫時性失敗被誤判成毒訊息、過早送進 DLQ；太大（例如 100）會讓真正的毒訊息浪費大量 consumer 重試。多數 task queue 設 3-5 是合理起點 — 足以吸收幾次暫時性失敗、又不至於讓確定性失敗的訊息空轉太久。</p>
<h2 id="message-size-限制與-extended-client">Message size 限制與 extended client</h2>
<p>SQS 單則訊息上限是 256 KB（含 message body 與 attributes）。這對純事件通知、id 引用、小型 payload 足夠、但對「訊息本身要攜帶大檔案內容」的場景不夠 — 例如要傳一份報表、一張圖、一段長文字。直接的反模式是把大內容塞進 message body、撞上 256 KB 限制後 <code>SendMessage</code> 直接報錯。</p>
<p>標準解是 claim-check 模式：大 payload 寫到 S3、訊息只攜帶 S3 的物件引用（bucket + key）、consumer 收到訊息後再去 S3 取內容。AWS 提供的 Extended Client Library（Java / Python 等 SDK）把這個模式封裝起來 — <code>SendMessage</code> 時若 payload 超過門檻、library 自動把內容寫 S3、訊息只帶 pointer；consumer 端 <code>ReceiveMessage</code> 時 library 自動從 S3 取回、對應用程式碼透明。</p>
<p>選擇門檻時要把 S3 的 request 成本與 latency 算進來：每則大訊息變成「一次 S3 PUT + 一次 SQS Send」、consumer 端「一次 SQS Receive + 一次 S3 GET」。對大多數 payload 都超過 256 KB 的 queue、這是必要成本；對 payload 多數很小、偶爾爆量的 queue、extended client 只在超門檻時走 S3、混合成本可接受。Payload 普遍很大且高頻的場景、要重新評估 SQS 是否適合 — 可能該改用 streaming（Kinesis / Kafka）或乾脆讓 producer / consumer 直接交換 S3 引用、SQS 只傳通知。</p>
<h2 id="cost按-request-計費每一次操作都是一個-request">Cost：按 request 計費，每一次操作都是一個 request</h2>
<p>SQS 的計費模型是 per-request、不是 per-message-stored、也沒有固定月費。每一次 API call — <code>SendMessage</code>、<code>ReceiveMessage</code>（含空回）、<code>DeleteMessage</code>、<code>ChangeMessageVisibility</code> — 都算一個 request。這個模型對成本估算的影響是：帳單由「操作次數」驅動、而非「訊息量」或「儲存時長」。一則訊息從 producer 到 consumer 的最小生命週期是 send（1）+ receive（1）+ delete（1）= 3 個 request；空輪詢、retry、visibility 延長都會額外加 request。</p>
<p>兩個降低 request 數的主要手段：</p>
<p>第一是 batch 操作。<code>SendMessageBatch</code> 與 <code>DeleteMessageBatch</code> 一次最多打包 10 則、而 SQS 把一個 batch call 算作一個 request（實際計費以 64 KB 為一個 request 單位、一個 batch 在此範圍內仍是少數 request）。把 10 則訊息的 send 從 10 個 request 壓成 1 個 batch request、在高頻 queue 上是數量級的成本差異：</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">aws sqs send-message-batch <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --entries <span class="s1">&#39;Id=m1,MessageBody=a&#39;</span> <span class="s1">&#39;Id=m2,MessageBody=b&#39;</span></span></span></code></pre></div><p>第二是 long polling 消滅空 request — 前面 polling 段已經展開。低流量 queue 的帳單若異常高、第一個要查的就是有沒有開 long polling、consumer 是不是在 short polling 下高頻空轉。</p>
<p>Data transfer cost 只在跨 region 時出現 — 同 region 內 producer / consumer 與 SQS 之間的傳輸不計流量費。把 producer、consumer、queue 放在同一個 region 是預設、跨 region 設計要把 egress 成本明確算進來。FIFO queue 的 per-request 單價比 standard 高、是用成本換 ordering 與去重保證 — 不需要嚴格順序的場景用 standard、把這筆溢價省下來。</p>
<p>Rapid7 的規模參考點說明這個計費模型在極端規模下的份量：Rapid7 公開引述 SQS 撐住「每天數十億則訊息」。在這個量級、per-request 計費乘以訊息數是一筆需要認真建模的成本 — batch、long polling、避免不必要的 visibility 延長、控制 retry 次數、每一項節省都被訊息量放大。SQS 在數十億級可用、但成本結構必須被當作架構參數對待、不是事後才看帳單。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="故障一visibility-timeout-短於處理時間訊息被重複處理">故障一：visibility timeout 短於處理時間，訊息被重複處理</h3>
<p><strong>徵兆</strong>：consumer log 顯示同一個 message id 在短時間內被處理多次、下游出現重複的副作用（重複扣款、重複寄信、重複寫入）；CloudWatch 的 <code>ApproximateNumberOfMessagesNotVisible</code>（in-flight 數）異常高、<code>NumberOfMessagesReceived</code> 遠大於 <code>NumberOfMessagesDeleted</code>。</p>
<p><strong>根因</strong>：visibility timeout 設定值低於 consumer 實際處理單則訊息的時間。訊息在 consumer 還沒處理完、還沒呼叫 <code>DeleteMessage</code> 之前、timeout 就到期、訊息重新可見、被另一個 consumer（或同一個 consumer 的下一輪 poll）領走。新建 queue 的 default 是 30 秒 — 處理時間長於此就會踩到：</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">aws sqs get-queue-attributes <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --queue-url &lt;url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --attribute-names VisibilityTimeout
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 看到 30 而 consumer 處理時間 &gt; 30s，就是這個問題</span></span></span></code></pre></div><p><strong>修法</strong>：把 visibility timeout 對齊 consumer 處理時間的 p99 加緩衝、用 <code>set-queue-attributes</code> 調高；處理時間變異大的長任務改用 <code>ChangeMessageVisibility</code> heartbeat 在處理中動態延長。同時、因為 SQS standard 是 at-least-once、重複投遞在故障與 retry 下本來就會發生、consumer 的處理邏輯必須冪等 — 對齊 visibility timeout 降低重複頻率、冪等性才是真正消除重複副作用的防線。</p>
<h3 id="故障二short-polling-預設導致低流量-queue-帳單異常">故障二：short polling 預設導致低流量 queue 帳單異常</h3>
<p><strong>徵兆</strong>：一個訊息量很低的 queue、月度 SQS 帳單卻很高；CloudWatch 顯示 <code>NumberOfEmptyReceives</code> 佔 <code>ReceiveMessage</code> 總數的絕大比例 — 大量 request 是空回。</p>
<p><strong>根因</strong>：consumer 走 short polling（<code>WaitTimeSeconds=0</code>、預設值）、在 queue 空的時候緊密地反覆發 <code>ReceiveMessage</code>、每次都立即空回、每次都計一個 request。流量越低、空回比例越高、帳單越是花在「問有沒有訊息」上。</p>
<p><strong>修法</strong>：在 queue-level 設 <code>ReceiveMessageWaitTimeSeconds=20</code> 讓所有 receive 預設走 long polling、或在每個 <code>ReceiveMessage</code> 帶 <code>--wait-time-seconds 20</code>。Queue-level 設定更穩、不依賴每個 consumer 記得帶參數。設定後 consumer 在 queue 空時會 hold 住連線最多 20 秒、空 request 數量級下降、帳單同步下降。同時確認 consumer 的 socket timeout 大於 20 秒、避免 client 先於 SQS 斷線。</p>
<h3 id="故障三lambda-batch-部分失敗整批訊息被重投">故障三：Lambda batch 部分失敗，整批訊息被重投</h3>
<p><strong>徵兆</strong>：一個 batch 裡只有少數訊息處理失敗、但整批訊息（含已成功的）全部回到 queue 重新處理；下游對已成功的訊息出現重複副作用；DLQ 累積速度遠超實際毒訊息數量。</p>
<p><strong>根因</strong>：Lambda event source mapping 的 default 行為是「整批成敗一體」— 函式只要拋出錯誤、整個 batch 被視為失敗、所有訊息（包含已經處理成功的）都不會被刪除、全部重新可見重投。Batch size 越大、一則失敗拖累的成功訊息越多。</p>
<p><strong>修法</strong>：啟用 partial batch response — event source mapping 設 <code>ReportBatchItemFailures</code>、handler 返回時只回報失敗的 message id 清單、SQS 只把這些重投、已成功的正常刪除。這把失敗的爆炸半徑從「整批」縮到「真正失敗的那幾則」。配合縮小 batch size 進一步降低單批風險、並確保 handler 冪等以承受不可避免的重投。Handler 必須正確實作 partial response 的返回格式 — 漏回報某則失敗會讓它被當成成功刪除、訊息靜默遺失。</p>
<h3 id="故障四maxreceivecount-設定不當毒訊息空轉或誤判">故障四：maxReceiveCount 設定不當，毒訊息空轉或誤判</h3>
<p><strong>徵兆</strong>：兩種相反的故障形狀。一是 DLQ 幾乎為空但主 queue 有訊息反覆重試數十次、consumer log 同一 message id 重複出現、佔用處理容量 — maxReceiveCount 設太大。二是 DLQ 快速累積大量其實沒問題的訊息、redrive 回去又能正常處理 — maxReceiveCount 設太小、把下游短暫抖動誤判成毒訊息。</p>
<p><strong>根因</strong>：redrive policy 沒設、或 <code>maxReceiveCount</code> 與「暫時性失敗的正常重試次數」不匹配。沒設 redrive policy 時毒訊息無限重投；設太大時毒訊息空轉太久才進 DLQ；設太小時正常訊息在下游抖動期間被過早判死。</p>
<p><strong>修法</strong>：對主 queue 設 redrive policy、<code>maxReceiveCount</code> 取 3-5 作為起點 — 足以吸收幾次暫時性失敗、又不讓確定性失敗的訊息空轉太久。觀察 DLQ 的累積模式再微調：DLQ 累積的多是「下游修好後 redrive 能成功」的訊息就調高、累積的多是「redrive 回去又進 DLQ」的真毒訊息就維持或調低。對 DLQ 設 CloudWatch alarm 監控 <code>ApproximateNumberOfMessagesVisible</code>、累積超過閾值就告警人工介入、區分 redrive vs 丟棄。</p>
<h3 id="故障五fifo-queue-撞上吞吐上限">故障五：FIFO queue 撞上吞吐上限</h3>
<p><strong>徵兆</strong>：把 standard queue 換成 FIFO 取得 ordering 後、高峰流量下 producer 端開始收到 throttling、訊息積壓、<code>SendMessage</code> 報限流錯誤；吞吐怎麼加 consumer 都上不去。</p>
<p><strong>根因</strong>：FIFO queue 為了維持順序與去重、吞吐遠低於 standard。FIFO 的基礎吞吐是每秒 300 則訊息（API call）、開啟 batching 後到每秒 3000 則。更關鍵的是順序保證的粒度在 <code>MessageGroupId</code> — 同一個 group 內的訊息嚴格串行處理、跨 group 才能並行。若所有訊息共用一個 group id、實際並行度退化成 1、無論加多少 consumer 都無法並行消化。</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"><span class="c1"># FIFO send 必須帶 MessageGroupId（決定順序與並行粒度）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws sqs send-message <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --queue-url &lt;fifo-url&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --message-body <span class="s2">&#34;ordered-1&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --message-group-id <span class="s2">&#34;group-a&#34;</span></span></span></code></pre></div><p><strong>修法</strong>：先確認是否真的需要全域順序 — 多數場景只需要「同一個實體（同一用戶、同一訂單）內部有序」、不需要跨實體有序。把 <code>MessageGroupId</code> 設成業務實體 id（用戶 id、訂單 id）、讓不同實體的訊息能跨 group 並行、吞吐隨 group 數量擴展。確定需要嚴格全域順序且吞吐撞頂的場景、FIFO 的設計上限就是天花板 — 此時要重新評估是否該換成 streaming（Kafka 的 partition 模型在 per-key 有序下提供更高並行）、或拆分 queue。不需要任何順序保證的場景、退回 standard queue、把 FIFO 的吞吐限制與成本溢價一起省掉。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<h3 id="跟-consumer-設計能力對接">跟 consumer 設計能力對接</h3>
<p>本文的 visibility timeout heartbeat、partial batch response、冪等處理都是 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計</a> 的具體落地 — consumer-design 講語言無關的 consumer 模式、本文是 SQS 上的實作形狀。retry 與 replay 的交接路徑見 <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">queue consumer retry replay handoff</a>。</p>
<h3 id="跟知識卡對位">跟知識卡對位</h3>
<p>DLQ 段對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>（毒訊息隔離）與 <a href="/blog/backend/knowledge-cards/dlq-drain/" data-link-title="DLQ Drain" data-link-desc="說明把 dead-letter queue 累積的訊息重新處理或排空的受控流程">dlq drain</a>（DLQ 排空）兩張卡 — SQS 的 redrive policy + maxReceiveCount 是這兩個概念在 managed queue 上的具體機制。visibility timeout 的 in-flight 概念見 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a>。</p>
<h3 id="跟-case-對位">跟 case 對位</h3>
<p>visibility timeout 與 Lambda event source 的 ramp-up 行為來自 <a href="/blog/backend/03-message-queue/cases/sqs-capital-one-visibility-timeout/" data-link-title="3.C50 Capital One：Visibility timeout 設計與 Lambda event source" data-link-desc="Capital One tech blog 講 SQS &#43; Lambda：visibility timeout 應略高於最大處理時間、Lambda 初 5 個 long polling、可擴 60/min。">3.C50 Capital One</a>；at-least-once + DLQ 在工作排程的取捨來自 <a href="/blog/backend/03-message-queue/cases/sqs-airbnb-dynein-delayed-jobs/" data-link-title="3.C48 Airbnb Dynein：SQS 分散式延遲任務排程" data-link-desc="Airbnb 用 SQS at-least-once &#43; DLQ 取代 Resque 單 Redis 限制、每 scheduler 1000 QPS、SQS wrap DynamoDB 處理 &gt; 15 分鐘 delay。">3.C48 Airbnb Dynein</a>；per-request cost 在極端規模的份量來自 <a href="/blog/backend/03-message-queue/cases/sqs-rapid7-scale-billion-messages/" data-link-title="3.C59 Rapid7：SQS 100 億 message/day 規模" data-link-desc="Rapid7 公開引述：SQS 撐 10s of billions of messages per day、是架構關鍵元件、scale 量級的具體參考。">3.C59 Rapid7</a>。</p>
<h3 id="何時-revisit">何時 revisit</h3>
<p>FIFO 吞吐撞頂、需要 replay / streaming、或 cost 在 streaming 模型下更划算時、回 <a href="/blog/backend/03-message-queue/vendors/aws-sqs/" data-link-title="AWS SQS" data-link-desc="AWS managed queue、簡單可靠、無 ordering（standard）">SQS overview 的「何時改走其他服務」</a> 重新選型。跨雲 managed queue 的對照見 <a href="/blog/backend/03-message-queue/vendors/google-pubsub/" data-link-title="Google Cloud Pub/Sub" data-link-desc="GCP managed pub/sub、global routing、push/pull">Google Pub/Sub</a>。</p>
]]></content:encoded></item><item><title>4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本</title><link>https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/cases/observability-cost-governance-at-scale/</guid><description>&lt;p>觀測成本治理案例來自多家企業的共同經驗：觀測平台帳單每季成長 30%，管理層問「為什麼監控這麼貴」但沒人能歸因。問題的核心不是「花太多」而是「花在哪不知道」— 沒有 per-team cost attribution 的觀測平台，成本優化只能靠全域砍 retention 或降 sampling，兩者都會傷害觀測品質。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>這個案例綜合三個組織的經驗模式：&lt;/p>
&lt;p>一家中型 SaaS 公司用 Datadog 做全端觀測（APM + logs + metrics + RUM）。月帳單從 $15K 成長到 $60K，兩年內四倍。CFO 問 CTO「這筆錢買到什麼」，CTO 轉問 platform team，platform team 說不出哪些團隊佔多少。&lt;/p>
&lt;p>一家金融科技公司自建 Grafana Stack（Prometheus + Loki + Tempo + Mimir）。自建沒有 SaaS 帳單，但 Kubernetes 節點跟 storage 的成本持續增加。infra team 知道 Mimir 的 storage 在成長，但不知道是哪些 metric label 造成的 cardinality 爆炸。&lt;/p>
&lt;p>一家遊戲公司用 CloudWatch 做 AWS 原生觀測。Logs 的 ingestion 費用佔帳單 70%，但追查後發現 90% 是 debug-level log，只在排錯時用到，平常沒人查。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="沒有-cost-attribution">沒有 cost attribution&lt;/h3>
&lt;p>觀測帳單通常是 organization-level 的一筆支出。SaaS 帳單按 hosts、custom metrics、log volume、APM spans 計費；自建平台按 compute 跟 storage 計費。兩種模式都缺少「這些費用是哪個 team / service 造成的」的歸因。&lt;/p>
&lt;p>沒有 attribution 的後果是所有優化都是全域操作 — 砍 retention 從 30 天到 7 天影響所有人，降 sampling 從 100% 到 10% 影響所有服務。需要觀測資料的團隊被平均到成本節省裡，不需要的團隊搭便車。&lt;/p>
&lt;h3 id="cardinality-爆炸">Cardinality 爆炸&lt;/h3>
&lt;p>Metrics 成本的主要 driver 是 cardinality — unique label combination 的數量。常見的 cardinality 爆炸來源：&lt;/p>
&lt;ul>
&lt;li>把 user ID 或 request ID 放進 metric label（每個 unique user 產生一組 series）&lt;/li>
&lt;li>動態的 endpoint path（&lt;code>/api/users/123&lt;/code> 每個 user ID 是一個 label value）&lt;/li>
&lt;li>多租戶 label 過細（tenant × region × service × endpoint 的笛卡兒積）&lt;/li>
&lt;/ul>
&lt;p>一個失控的 label 可以讓 series 數量從 10 萬跳到 1000 萬。SaaS 的計費是 per custom metric，自建的代價是 Prometheus / Mimir 的 memory 跟 storage。&lt;/p>
&lt;h3 id="log-volume-失控">Log volume 失控&lt;/h3>
&lt;p>Debug-level log 在開發階段有用，但 production 環境裡通常只在排錯時被查。全量 debug log 送進 hot tier（Elasticsearch、Loki、CloudWatch Logs）的 ingestion 跟 storage 成本是最大的 log 成本來源。&lt;/p>
&lt;p>問題是沒人敢降 debug log — 「萬一出事需要 debug log 怎麼辦」。恐懼驅動的 log level 設定讓 log volume 只升不降。&lt;/p></description><content:encoded><![CDATA[<p>觀測成本治理案例來自多家企業的共同經驗：觀測平台帳單每季成長 30%，管理層問「為什麼監控這麼貴」但沒人能歸因。問題的核心不是「花太多」而是「花在哪不知道」— 沒有 per-team cost attribution 的觀測平台，成本優化只能靠全域砍 retention 或降 sampling，兩者都會傷害觀測品質。</p>
<h2 id="業務背景">業務背景</h2>
<p>這個案例綜合三個組織的經驗模式：</p>
<p>一家中型 SaaS 公司用 Datadog 做全端觀測（APM + logs + metrics + RUM）。月帳單從 $15K 成長到 $60K，兩年內四倍。CFO 問 CTO「這筆錢買到什麼」，CTO 轉問 platform team，platform team 說不出哪些團隊佔多少。</p>
<p>一家金融科技公司自建 Grafana Stack（Prometheus + Loki + Tempo + Mimir）。自建沒有 SaaS 帳單，但 Kubernetes 節點跟 storage 的成本持續增加。infra team 知道 Mimir 的 storage 在成長，但不知道是哪些 metric label 造成的 cardinality 爆炸。</p>
<p>一家遊戲公司用 CloudWatch 做 AWS 原生觀測。Logs 的 ingestion 費用佔帳單 70%，但追查後發現 90% 是 debug-level log，只在排錯時用到，平常沒人查。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="沒有-cost-attribution">沒有 cost attribution</h3>
<p>觀測帳單通常是 organization-level 的一筆支出。SaaS 帳單按 hosts、custom metrics、log volume、APM spans 計費；自建平台按 compute 跟 storage 計費。兩種模式都缺少「這些費用是哪個 team / service 造成的」的歸因。</p>
<p>沒有 attribution 的後果是所有優化都是全域操作 — 砍 retention 從 30 天到 7 天影響所有人，降 sampling 從 100% 到 10% 影響所有服務。需要觀測資料的團隊被平均到成本節省裡，不需要的團隊搭便車。</p>
<h3 id="cardinality-爆炸">Cardinality 爆炸</h3>
<p>Metrics 成本的主要 driver 是 cardinality — unique label combination 的數量。常見的 cardinality 爆炸來源：</p>
<ul>
<li>把 user ID 或 request ID 放進 metric label（每個 unique user 產生一組 series）</li>
<li>動態的 endpoint path（<code>/api/users/123</code> 每個 user ID 是一個 label value）</li>
<li>多租戶 label 過細（tenant × region × service × endpoint 的笛卡兒積）</li>
</ul>
<p>一個失控的 label 可以讓 series 數量從 10 萬跳到 1000 萬。SaaS 的計費是 per custom metric，自建的代價是 Prometheus / Mimir 的 memory 跟 storage。</p>
<h3 id="log-volume-失控">Log volume 失控</h3>
<p>Debug-level log 在開發階段有用，但 production 環境裡通常只在排錯時被查。全量 debug log 送進 hot tier（Elasticsearch、Loki、CloudWatch Logs）的 ingestion 跟 storage 成本是最大的 log 成本來源。</p>
<p>問題是沒人敢降 debug log — 「萬一出事需要 debug log 怎麼辦」。恐懼驅動的 log level 設定讓 log volume 只升不降。</p>
<h3 id="trace-sampling-恐懼">Trace sampling 恐懼</h3>
<p>類似的恐懼存在於 trace sampling — 「如果剛好那筆有問題的 request 被 sample 掉怎麼辦」。100% tracing 的成本在中等規模（每秒數萬 request）就開始顯著。</p>
<h2 id="解法">解法</h2>
<h3 id="cost-attribution-by-team--service">Cost attribution by team / service</h3>
<p>第一步是讓成本可見，歸因先於優化。</p>
<p>SaaS 平台：用 Datadog 的 usage attribution 或 Grafana Cloud 的 usage reporting 把 ingestion 按 service tag / team tag 拆分。每個 team 看到自己的 metric series、log volume 跟 span 數量。</p>
<p>自建平台：在 Mimir / Loki 的 tenant 維度或 Prometheus 的 namespace 維度拆分 storage 跟 query cost。用 <a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a> 的框架把 infra cost 按 service ownership 分配。</p>
<p>Attribution 本身就能驅動行為改變 — 當團隊看到自己佔了 40% 的 log volume、而且 95% 是 debug level 時，他們會主動調 log level。</p>
<h3 id="cardinality-budget-per-team">Cardinality budget per team</h3>
<p>Attribution 之後，為每個 team / service 設定 cardinality budget（active series 上限）。超出 budget 的 series 進入 review 流程 — team 決定哪些 label 可以 aggregate 或移除，而非由 platform 單方面 drop。</p>
<p>Budget 的設定依據是 baseline measurement + growth rate，不是拍腦袋。先觀察 3 個月的 cardinality 趨勢，把 budget 設在 baseline 的 1.5 倍，每季 review。</p>
<h3 id="log-tiering">Log tiering</h3>
<p>把 log 從「全部進 hot tier」改成分層：</p>
<table>
  <thead>
      <tr>
          <th>Log level</th>
          <th>目的地</th>
          <th>Retention</th>
          <th>查詢延遲</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Error / Warn</td>
          <td>Hot tier（Loki / Elasticsearch）</td>
          <td>30 天</td>
          <td>即時</td>
      </tr>
      <tr>
          <td>Info</td>
          <td>Warm tier（壓縮 + 延遲查詢）</td>
          <td>14 天</td>
          <td>秒到分鐘</td>
      </tr>
      <tr>
          <td>Debug</td>
          <td>Cold archive（object storage）</td>
          <td>7 天</td>
          <td>分鐘到小時</td>
      </tr>
  </tbody>
</table>
<p>Debug log 仍然保留，但不進昂貴的 hot tier。需要排錯時從 cold archive 拉回 — 多等幾分鐘的代價遠低於全量 hot tier 的持續成本。</p>
<h3 id="adaptive-sampling">Adaptive sampling</h3>
<p>Trace sampling 從 uniform 改成 adaptive：</p>
<ul>
<li>錯誤 request 100% 保留</li>
<li>高 latency request（&gt; p99）100% 保留</li>
<li>正常 request 依 traffic volume adaptive sampling（高流量 endpoint 低 sample rate、低流量 endpoint 高 sample rate）</li>
</ul>
<p>Adaptive sampling 保留了排錯最需要的 trace（error 跟 outlier），砍的是正常 request 的重複 trace。</p>
<h2 id="取捨">取捨</h2>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>不治理</th>
          <th>治理後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>成本趨勢</td>
          <td>隨 traffic 超線性成長</td>
          <td>跟 traffic 線性成長或低於線性</td>
      </tr>
      <tr>
          <td>觀測覆蓋</td>
          <td>全量（但可能是低品質的全量）</td>
          <td>分層（high-value 資料保留全量、low-value 降級）</td>
      </tr>
      <tr>
          <td>Debug 體驗</td>
          <td>所有資料都在 hot tier、查得快</td>
          <td>部分資料要從 cold archive 拉、多等幾分鐘</td>
      </tr>
      <tr>
          <td>團隊自主性</td>
          <td>無限制（cardinality 跟 log level 隨意）</td>
          <td>有 budget 跟 policy 約束</td>
      </tr>
      <tr>
          <td>治理人力</td>
          <td>零（直到帳單爆炸才開始）</td>
          <td>需要 platform team 持續維護 attribution + budget + policy</td>
      </tr>
  </tbody>
</table>
<p>治理的最大風險是「砍過頭」— 在事故期間發現 debug log 被移到 cold archive 查不到、或 trace 被 sample 掉找不到問題 request。Adaptive sampling 跟 error retention 100% 是安全網，但安全網的設計本身需要定期 review（例如 error 的定義是否涵蓋了所有異常模式）。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><a href="/blog/backend/04-observability/cost-attribution/" data-link-title="4.15 Cost Attribution / Chargeback" data-link-desc="把 observability 成本拆到團隊、產品、環境維度">4.15 Cost Attribution</a>：per-team cost visibility 是治理的起點。</li>
<li><a href="/blog/backend/04-observability/cardinality-cost-governance/" data-link-title="4.7 Cardinality 治理與成本邊界" data-link-desc="把 metric / log / trace 的 cardinality 與成本作為平台一級治理議題">4.7 Cardinality 治理</a>：cardinality budget 跟 label review 的操作流程。</li>
<li><a href="/blog/backend/04-observability/telemetry-pipeline/" data-link-title="4.11 Telemetry Pipeline 架構" data-link-desc="把 log / metric / trace 的 agent → collector → ingest → storage → query 分層治理">4.11 Telemetry Pipeline</a>：log tiering 跟 adaptive sampling 是 pipeline 的 routing 跟 processing 層配置。</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>觀測帳單每季成長 &gt; 20%，但服務的 request volume 成長遠小於此 — cardinality 或 log volume 可能在失控成長</li>
<li>管理層問「監控花多少錢、誰在用」但沒人能回答</li>
<li>曾經做過「全域降 retention」或「全域降 sampling」的成本優化，但幾個月後成本回升</li>
<li>Platform team 花大量時間處理「Prometheus OOM」或「Elasticsearch disk full」而非改善觀測品質</li>
<li>團隊的 debug log level 在 production 預設開著，理由是「不知道什麼時候需要」</li>
</ul>
]]></content:encoded></item><item><title>4.18 Prompt caching 工程實務：cost / latency 最大槓桿</title><link>https://tarrragon.github.io/blog/llm/04-applications/prompt-caching-engineering/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/llm/04-applications/prompt-caching-engineering/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">Prompt cache&lt;/a> 把重複 prefix 的計算結果在 LLM 服務端跨 request 持久化、後續 query 跳過 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill&lt;/a> 階段。Anthropic / OpenAI / Bedrock / Gemini 都列為 cost 跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT&lt;/a> 的最大單一槓桿 — 90% cost 折扣 + 顯著 latency 改善。本章把 prompt caching 的運作機制、設計原則、coding agent / long-context 場景的 pattern、常見 anti-pattern 拆成可操作的工程實務。&lt;/p>
&lt;p>注意三層 cache 概念的層次差異（&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">prompt cache 卡片&lt;/a> 有完整對比表）：&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> 是單次推論內、過去 token 的 K/V 暫存（autoregressive 才省重算）；&lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache&lt;/a> 是同一推論伺服器內跨 request 共用 KV cache；&lt;strong>prompt cache（本章聚焦）&lt;/strong> 是雲端 LLM API 商業 feature、跨 request 跨時間、有 TTL。三者不同層、要區分。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>解釋 prompt cache 跟 &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache&lt;/a> 的層次差異。&lt;/li>
&lt;li>對 coding agent / RAG / long-conversation 場景設計 cache breakpoint。&lt;/li>
&lt;li>估算自己應用開 prompt cache 的 cost / latency 收益。&lt;/li>
&lt;li>看到「cache 不命中」訊號時、能定位 anti-pattern 並修。&lt;/li>
&lt;/ol>
&lt;h2 id="prompt-cache-怎麼運作">Prompt cache 怎麼運作&lt;/h2>
&lt;p>LLM 推論的 prefill 階段對整個 prompt 算 KV cache、是長 prompt 的主要 latency 跟 compute 成本：&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">無 cache：
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> Request 1：[10K system prompt] + [tool schema 5K] + [user query 500] = 15.5K prefill
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> Request 2：[10K system prompt] + [tool schema 5K] + [user query 700] = 15.7K prefill
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> → 兩次都付 15K prefill 成本&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開 prompt cache 後：&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">Prompt cache</a> 把重複 prefix 的計算結果在 LLM 服務端跨 request 持久化、後續 query 跳過 <a href="/blog/llm/knowledge-cards/prefill/" data-link-title="Prefill" data-link-desc="Prompt 首次處理時的計算階段：把整段輸入跑過模型、產生 KV cache">prefill</a> 階段。Anthropic / OpenAI / Bedrock / Gemini 都列為 cost 跟 <a href="/blog/llm/knowledge-cards/ttft/" data-link-title="TTFT" data-link-desc="Time To First Token：送出 prompt 到第一個 token 出現的等待時間">TTFT</a> 的最大單一槓桿 — 90% cost 折扣 + 顯著 latency 改善。本章把 prompt caching 的運作機制、設計原則、coding agent / long-context 場景的 pattern、常見 anti-pattern 拆成可操作的工程實務。</p>
<p>注意三層 cache 概念的層次差異（<a href="/blog/llm/knowledge-cards/prompt-cache/" data-link-title="Prompt Cache" data-link-desc="重複出現的 prompt prefix 在推論伺服器或 LLM 服務端被 cache、後續 query 跳過 prefill、大幅降 cost 跟 TTFT">prompt cache 卡片</a> 有完整對比表）：<a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> 是單次推論內、過去 token 的 K/V 暫存（autoregressive 才省重算）；<a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache</a> 是同一推論伺服器內跨 request 共用 KV cache；<strong>prompt cache（本章聚焦）</strong> 是雲端 LLM API 商業 feature、跨 request 跨時間、有 TTL。三者不同層、要區分。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>解釋 prompt cache 跟 <a href="/blog/llm/knowledge-cards/kv-cache/" data-link-title="KV Cache" data-link-desc="已處理 token 的 attention 中間結果暫存：避免重算、加速後續生成">KV cache</a> / <a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache</a> 的層次差異。</li>
<li>對 coding agent / RAG / long-conversation 場景設計 cache breakpoint。</li>
<li>估算自己應用開 prompt cache 的 cost / latency 收益。</li>
<li>看到「cache 不命中」訊號時、能定位 anti-pattern 並修。</li>
</ol>
<h2 id="prompt-cache-怎麼運作">Prompt cache 怎麼運作</h2>
<p>LLM 推論的 prefill 階段對整個 prompt 算 KV cache、是長 prompt 的主要 latency 跟 compute 成本：</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">無 cache：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Request 1：[10K system prompt] + [tool schema 5K] + [user query 500] = 15.5K prefill
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Request 2：[10K system prompt] + [tool schema 5K] + [user query 700] = 15.7K prefill
</span></span><span class="line"><span class="ln">4</span><span class="cl">  → 兩次都付 15K prefill 成本</span></span></code></pre></div><p>開 prompt cache 後：</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">Request 1：[10K system + 5K tool schema] | cache_control | + [user query 500]
</span></span><span class="line"><span class="ln">2</span><span class="cl">  → 算出 prefix 的 KV cache、寫進服務端 cache（付 1.25× cost）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  → 後段 prefill 500 token
</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">Request 2（5 分鐘內）：[10K system + 5K tool schema] | + [user query 700]
</span></span><span class="line"><span class="ln">6</span><span class="cl">  → 服務端命中 cache、跳過 prefix 的 prefill（付 0.1× cost = 90% 折扣）
</span></span><span class="line"><span class="ln">7</span><span class="cl">  → 只 prefill 700 token
</span></span><span class="line"><span class="ln">8</span><span class="cl">  → TTFT 大幅降低</span></span></code></pre></div><p>關鍵運作細節：</p>
<ol>
<li><strong>Cache key = prefix 的 token sequence</strong>：完全相同的 token sequence 才命中、差一個 token 就 miss</li>
<li><strong>TTL（time-to-live）</strong>：cache 過一段時間（多數 5 min）自動失效、要 ext 1h 通常付額外 cost</li>
<li><strong>Write 比原價略貴、Read 大幅打折</strong>：Anthropic 模型 write 1.25×、read 0.1×；OpenAI 模型 read 0.5×</li>
<li><strong>Minimum cacheable size</strong>：通常 1K-4K token 起跳、短 prompt 不適合</li>
<li><strong>Cache 範圍</strong>：跨 request、跨 conversation、跨 session、但同一 model + 同一 region</li>
</ol>
<h2 id="cache-breakpoint-設計">Cache breakpoint 設計</h2>
<p>Anthropic 用 <code>cache_control</code> 標記顯式 breakpoint、OpenAI 用自動偵測。但設計原則一致：<strong>把不變的內容放 prefix、變動的放後面</strong>。</p>
<p>典型 coding agent 的 prompt 結構：</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">[1. System prompt]：agent 角色、規則、輸出格式             ← 不變
</span></span><span class="line"><span class="ln">2</span><span class="cl">[2. Tool schema]：所有 tool 的 spec                       ← 不變（除非加新 tool）
</span></span><span class="line"><span class="ln">3</span><span class="cl">[3. Skill registry / playbook]：known recipes              ← 半變（偶爾更新）
</span></span><span class="line"><span class="ln">4</span><span class="cl">[4. Codebase context]：固定載入的核心檔案                  ← 半變
</span></span><span class="line"><span class="ln">5</span><span class="cl">       ↓ cache_control breakpoint ↑
</span></span><span class="line"><span class="ln">6</span><span class="cl">[5. Conversation history]：過去回合                       ← 變動
</span></span><span class="line"><span class="ln">7</span><span class="cl">[6. Current user query]：當前 query                       ← 變動
</span></span><span class="line"><span class="ln">8</span><span class="cl">[7. Current tool result]：剛跑完的 tool output             ← 變動</span></span></code></pre></div><p>Breakpoint 放在「不變 vs 變動」交界處、讓 [1-4] 永遠 cache hit。</p>
<p>Anthropic 最多 4 個 breakpoint、可分層：</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">breakpoint 1（最早）：[system prompt] → 永久 cache
</span></span><span class="line"><span class="ln">2</span><span class="cl">breakpoint 2：       [+ tool schema] → 永久 cache
</span></span><span class="line"><span class="ln">3</span><span class="cl">breakpoint 3：       [+ skill registry] → 半永久 cache
</span></span><span class="line"><span class="ln">4</span><span class="cl">breakpoint 4（最晚）：[+ recent stable context] → 短期 cache
</span></span><span class="line"><span class="ln">5</span><span class="cl">[後段]：             variable content（不 cache）</span></span></code></pre></div><p>每個 breakpoint 各自命中 / miss、layered cache 讓「加新 skill」只 invalidate breakpoint 3 之後、不影響 breakpoint 1-2。</p>
<h2 id="場景-1coding-agent">場景 1：Coding agent</h2>
<p>Coding agent 是 prompt cache 命中區 — system prompt + tool schema 動輒 10K-30K token、每個 user turn 都重用。</p>
<p>收益估算（200K context 模型、10K scaffold、5K user query、3K answer）：</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">無 cache：
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  每 turn input cost = (10K + 5K) × $3/M = $0.045
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  每 turn TTFT = 10K-15K prefill time（200-400ms）
</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">開 cache：
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">  Turn 1（write）：(10K × 1.25 + 5K) × $3/M = $0.0525
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">  Turn 2-N（read）：(10K × 0.1 + 5K) × $3/M = $0.018
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  TTFT：read 階段省掉 10K prefill、只剩 5K
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">10 turns 的累計 cost：
</span></span><span class="line"><span class="ln">11</span><span class="cl">  無 cache：10 × $0.045 = $0.45
</span></span><span class="line"><span class="ln">12</span><span class="cl">  開 cache：$0.0525 + 9 × $0.018 = $0.215
</span></span><span class="line"><span class="ln">13</span><span class="cl">  → 節省 52%</span></span></code></pre></div><p>長對話越長、cache 收益越大（cache write 是一次性成本）。</p>
<h2 id="場景-2rag--long-context">場景 2：RAG / long-context</h2>
<p>RAG 場景把 retrieved chunks 放 prefix、user query 放後面、可以 cache retrieved chunks：</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">[system prompt]
</span></span><span class="line"><span class="ln">2</span><span class="cl">       ↓ breakpoint 1（system 永久 cache）
</span></span><span class="line"><span class="ln">3</span><span class="cl">[retrieved chunks 來自 RAG]
</span></span><span class="line"><span class="ln">4</span><span class="cl">       ↓ breakpoint 2（同 chunks 在 5min 內 cache）
</span></span><span class="line"><span class="ln">5</span><span class="cl">[user query]</span></span></code></pre></div><p>注意：每次 retrieval 不同 chunks 就 cache miss、所以 cache 適合「同個對話多輪、retrieval 結果穩定」、不適合「每 query 都 fresh retrieve」；後者要回到 <a href="/blog/llm/knowledge-cards/retrieval-cost/" data-link-title="Retrieval Cost" data-link-desc="RAG 檢索帶來的 latency、token、embedding、reranker、LLM call 與維護成本，用來判斷增強是否划算">retrieval cost</a> 評估。</p>
<h2 id="場景-3long-document-qa">場景 3：Long document Q&amp;A</h2>
<p>讀者上傳 PDF / 文件、多輪問問題：</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">[system prompt]
</span></span><span class="line"><span class="ln">2</span><span class="cl">       ↓ breakpoint 1
</span></span><span class="line"><span class="ln">3</span><span class="cl">[完整文件內容（可能 100K token）]
</span></span><span class="line"><span class="ln">4</span><span class="cl">       ↓ breakpoint 2（文件永久 cache）
</span></span><span class="line"><span class="ln">5</span><span class="cl">[user query]</span></span></code></pre></div><p>第一次 query 付 1.25× 文件成本、後續 query 都 0.1×。100K 文件 + 10 個問題的場景下、節省極顯著（&gt; 80% cost）。</p>
<h2 id="常見-anti-pattern">常見 anti-pattern</h2>
<ol>
<li><strong>在 prefix 插入 timestamp / request-id</strong></li>
</ol>





<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">反例：System prompt: &#34;你是 coding assistant、當前時間 2026-05-12 16:30:42、...&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → 每秒不同 cache key、永遠 cache miss、付 1.25× write 不回本
</span></span><span class="line"><span class="ln">3</span><span class="cl">正解：把 timestamp 放後段、或省略（多數場景模型不需要精確時間）</span></span></code></pre></div><ol start="2">
<li><strong>在 prefix 動態插入 user metadata</strong></li>
</ol>





<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">反例：System prompt: &#34;User: alice@example.com, plan: premium、...&#34;
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → 每個 user 不同 cache、命中率低
</span></span><span class="line"><span class="ln">3</span><span class="cl">正解：User metadata 放後段、prefix 保持 user-agnostic</span></span></code></pre></div><ol start="3">
<li><strong>Tool schema 順序不固定</strong></li>
</ol>





<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">反例：每次 request 把 tool list 隨機 shuffle
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → 同樣 tool 但 token sequence 不同、cache miss
</span></span><span class="line"><span class="ln">3</span><span class="cl">正解：Tool list 順序固定、新加 tool 都 append 到末尾</span></span></code></pre></div><ol start="4">
<li><strong>太短的 prompt 也想 cache</strong></li>
</ol>





<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">反例：500 token system prompt 開 cache
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → 多數服務商 minimum 1K-4K、不到門檻不 cache、且 write cost 不回本
</span></span><span class="line"><span class="ln">3</span><span class="cl">正解：Cache 留給 &gt; 1K 的 prefix、短 prompt 不必開</span></span></code></pre></div><ol start="5">
<li><strong>混用 stream + cache 卻不檢查命中</strong></li>
</ol>





<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">反例：開 cache 後不檢查 response 的 cache_read_input_tokens 欄位
</span></span><span class="line"><span class="ln">2</span><span class="cl">   → 不知道實際命中率、可能 anti-pattern 已在燒 cost 沒察覺
</span></span><span class="line"><span class="ln">3</span><span class="cl">正解：監控 cache_read / cache_creation token 比例、低於 80% 命中率時 debug</span></span></code></pre></div><h2 id="cache-miss-訊號跟診斷">Cache miss 訊號跟診斷</h2>
<p>訊號：</p>
<ol>
<li><strong>Cost 比預期高</strong>：應該命中的場景仍付 full price</li>
<li><strong>TTFT 沒改善</strong>：cache hit 應該大幅降 TTFT、沒改善 = miss</li>
<li><strong>Response 的 usage 顯示 cache_read = 0</strong>：直接訊號</li>
</ol>
<p>診斷流程：</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">1. 印出 raw request 的 prefix（cache_control 之前）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. 比對連續兩次 request 的 prefix token sequence
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 找出差異位置（diff）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 移除 / 重構讓兩次 prefix 完全相同
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 跑 2-3 次 request、看 cache_read_input_tokens 是否上升</span></span></code></pre></div><p>常見差異：timestamp、request id、user id、tool list 順序、retrieved chunks 順序、conversation summary 變動。</p>
<h2 id="跟其他-cost-優化技巧的關係">跟其他 cost 優化技巧的關係</h2>
<table>
  <thead>
      <tr>
          <th>技巧</th>
          <th>攻擊的 cost / latency 來源</th>
          <th>跟 prompt cache 的關係</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/speculative-decoding/" data-link-title="Speculative Decoding" data-link-desc="用小模型猜未來 token、大模型並行驗證的加速技巧">Speculative decoding</a></td>
          <td>Generation 階段 token cost</td>
          <td>正交、可疊加</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/batching/" data-link-title="Batching" data-link-desc="多 request 一起跑、攤平 model load 成本：production LLM inference 的核心優化、決定 throughput vs latency 取捨">Batching</a></td>
          <td>Throughput per GPU</td>
          <td>Production 才用、跟 prompt cache 都用</td>
      </tr>
      <tr>
          <td><a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">Prefix cache</a></td>
          <td>同 server 跨 request 共用 KV cache</td>
          <td>本地推論伺服器特性、prompt cache 是雲端 API 商業 feature</td>
      </tr>
      <tr>
          <td>模型量化</td>
          <td>Generation tok/s</td>
          <td>正交、可疊加</td>
      </tr>
      <tr>
          <td>RAG 而非 long context</td>
          <td>Input token 量</td>
          <td>RAG + cache 可同時用</td>
      </tr>
  </tbody>
</table>
<h2 id="本地推論伺服器有沒有類似機制">本地推論伺服器有沒有類似機制</h2>
<p>Ollama / LM Studio / llama.cpp 自身的 prompt cache：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>機制</th>
          <th>範圍</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>llama.cpp</td>
          <td><code>--prompt-cache</code> flag、persistent file</td>
          <td>重複跑同樣 prompt 時跳過 prefill</td>
      </tr>
      <tr>
          <td>Ollama</td>
          <td>內建 prefix cache、跨 request 共用</td>
          <td>同 server 跨 request</td>
      </tr>
      <tr>
          <td>LM Studio</td>
          <td>同 Ollama 級別、視版本</td>
          <td>同上</td>
      </tr>
      <tr>
          <td>vLLM</td>
          <td>強 prefix cache（PagedAttention 設計支援）</td>
          <td>高併發 production</td>
      </tr>
  </tbody>
</table>
<p>本地推論的 cache 主要靠 <a href="/blog/llm/knowledge-cards/prefix-cache/" data-link-title="Prefix Cache" data-link-desc="把多個請求共用的前綴 prompt 的 KV cache 重用、省下重複 prefill 算力的優化、production 多用戶服務的常見設計">prefix cache</a> 機制、跟雲端 API 的 prompt cache 商業 feature 同源、但定價 / TTL / 顯式 control 是雲端 API 才有的 product layer。</p>
<h2 id="何時不適合用-prompt-cache">何時不適合用 prompt cache</h2>
<ol>
<li><strong>每 request prefix 必變</strong>：streaming 任務、每 query 都帶 fresh 上下文</li>
<li><strong>Single-shot 對話</strong>：用完就丟、沒有重複使用、write cost 不回本</li>
<li><strong>Prefix &lt; 1K token</strong>：不到 minimum、cache 不生效</li>
<li><strong>Cost 不敏感場景</strong>：個人小流量、cache 設計 overhead 大於收益</li>
<li><strong>本地推論為主</strong>：本地多用 prefix cache、prompt cache 是雲端 API 概念</li>
</ol>
<h2 id="何時過時--何時不過時">何時過時 / 何時不過時</h2>
<p><strong>不會過時的部分</strong>：</p>
<ul>
<li>「不變放 prefix、變動放後段」的設計原則</li>
<li>Cache breakpoint 分層（system / tool schema / skill / context）</li>
<li>Anti-pattern 分類（timestamp、user metadata、tool 順序）</li>
<li>Cache miss 診斷流程</li>
</ul>
<p><strong>會變的部分</strong>：</p>
<ul>
<li>各 vendor 的具體定價（write × / read × 折扣）</li>
<li>TTL（5min vs 1h）的可選性跟價格</li>
<li>Automatic vs explicit cache（OpenAI vs Anthropic 路線）</li>
<li>Breakpoint 上限數量</li>
<li>本地推論伺服器的 cache 功能（持續演化）</li>
</ul>
<h2 id="下一章">下一章</h2>
<p>下一章：<a href="/blog/llm/04-applications/agent-memory-architecture/" data-link-title="4.19 Agent memory 分層架構" data-link-desc="Agent 在 context window 之外管理長期狀態的設計：working / short-term / long-term episodic / semantic / procedural 五個層次、寫入時機、retrieval 設計、失敗模式">4.19 Agent memory 分層</a>、看 agent 如何在 context window 之外管理長期狀態。</p>
]]></content:encoded></item><item><title>Aurora Serverless v2 適用判斷：ACU 自動擴縮、混合 cluster 與何時不該用</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/serverless-v2-scaling/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/serverless-v2-scaling/</guid><description>&lt;p>Aurora Serverless v2 把 instance 的容量從「開機時固定的 instance class」改成「按負載秒級伸縮的 ACU」。它解的問題很具體：固定 provisioned cluster 在離峰時段付滿整台機器的錢、卻只用一小部分；尖峰來時又被 instance class 上限卡住。但 serverless v2 不是「比較便宜的 Aurora」——穩定高負載下它反而比同等 provisioned 貴。要不要用，取決於 workload 的負載形狀是否間歇、是否難預測。&lt;/p>
&lt;p>本文不是 Aurora overview（請看 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&amp;#43;75% 效能改善的 production 證據">Aurora vendor 頁&lt;/a>）— 而是 Serverless v2 的容量機制、設定與適用邊界的實作層教學。&lt;/p>
&lt;h2 id="核心機制acu-與秒級擴縮">核心機制：ACU 與秒級擴縮&lt;/h2>
&lt;p>Serverless v2 的容量單位是 ACU（Aurora Capacity Unit），一個 ACU 對應一組固定比例的記憶體與運算資源。cluster 不再綁定一個 instance class，而是設一個 ACU 區間（min / max），Aurora 依即時負載在區間內伸縮：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>屬性&lt;/th>
 &lt;th>Provisioned&lt;/th>
 &lt;th>Serverless v2&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>容量設定&lt;/td>
 &lt;td>固定 instance class（如 db.r6g.xlarge）&lt;/td>
 &lt;td>min / max ACU 區間&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>計費&lt;/td>
 &lt;td>按 instance 開機時數&lt;/td>
 &lt;td>按實際消耗的 ACU-秒&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>擴縮&lt;/td>
 &lt;td>手動改 instance class（有中斷）&lt;/td>
 &lt;td>秒級自動伸縮、無中斷&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>離峰成本&lt;/td>
 &lt;td>付滿整台&lt;/td>
 &lt;td>縮到 min ACU、只付低水位&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>適用負載&lt;/td>
 &lt;td>穩定、可預測&lt;/td>
 &lt;td>間歇、突發、難預測&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>擴縮行為&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>負載上升時 ACU 平滑增加、不需要切換 instance、無連線中斷&lt;/li>
&lt;li>負載下降時縮回低水位、但受 min ACU 下限約束&lt;/li>
&lt;li>min ACU 決定離峰的最低成本與「保留多少暖容量」；max ACU 決定尖峰的上限與成本天花板&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「ACU 對應的記憶體比例」「serverless v2 是否能縮到 0」「最小 ACU 粒度」這些屬 AWS vendor 規格、會隨版本演進（auto-pause 等能力陸續調整）、實作時 cross-verify 官方 doc 當前值。本文不含 production case 揭露的 ACU 配置數字。&lt;/p>&lt;/blockquote>
&lt;p>對應 knowledge card：&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;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">cost per request&lt;/a>。&lt;/p>
&lt;h2 id="min--max-acu-的設定權衡">min / max ACU 的設定權衡&lt;/h2>
&lt;p>min 與 max ACU 不是隨便設，兩端各自承擔不同風險。&lt;/p>
&lt;p>&lt;strong>min ACU 太低&lt;/strong>：離峰省錢，但流量回升時從很低的水位往上爬、爬升期間可能容量不足、且 buffer cache 在低 ACU 時被壓縮、回升後 cache 重新暖機、query latency 短暫升高。對延遲敏感、又有規律日週期的 workload，min ACU 不要壓到極限。&lt;/p>
&lt;p>&lt;strong>max ACU 太低&lt;/strong>：尖峰被天花板卡住、等同 provisioned 的 instance class 上限問題又回來。max ACU 要按「預期尖峰 + 餘量」設，並把它當成成本天花板來監控——max 設太高雖然不會平時就花錢，但失控 query（如缺索引的全表掃描）可能把 ACU 一路推到 max、帳單尖峰。&lt;/p>
&lt;p>&lt;strong>暖容量考量&lt;/strong>：min ACU 同時決定「保留多少隨時可用的暖容量」。完全不可預測、且要求第一個請求就低延遲的場景，min ACU 要留足暖機水位，不能為了省錢設到最低。&lt;/p>
&lt;h2 id="混合-clusterserverless--provisioned-並存">混合 cluster：serverless + provisioned 並存&lt;/h2>
&lt;p>Serverless v2 不是「整個 cluster 要嘛全 serverless、要嘛全 provisioned」。同一個 Aurora cluster 可以混用：writer 用 provisioned 保穩定、read replica 用 serverless v2 吸收讀取尖峰；或反過來。這讓 workload 的不同部分各取所需：&lt;/p></description><content:encoded><![CDATA[<p>Aurora Serverless v2 把 instance 的容量從「開機時固定的 instance class」改成「按負載秒級伸縮的 ACU」。它解的問題很具體：固定 provisioned cluster 在離峰時段付滿整台機器的錢、卻只用一小部分；尖峰來時又被 instance class 上限卡住。但 serverless v2 不是「比較便宜的 Aurora」——穩定高負載下它反而比同等 provisioned 貴。要不要用，取決於 workload 的負載形狀是否間歇、是否難預測。</p>
<p>本文不是 Aurora overview（請看 <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora vendor 頁</a>）— 而是 Serverless v2 的容量機制、設定與適用邊界的實作層教學。</p>
<h2 id="核心機制acu-與秒級擴縮">核心機制：ACU 與秒級擴縮</h2>
<p>Serverless v2 的容量單位是 ACU（Aurora Capacity Unit），一個 ACU 對應一組固定比例的記憶體與運算資源。cluster 不再綁定一個 instance class，而是設一個 ACU 區間（min / max），Aurora 依即時負載在區間內伸縮：</p>
<table>
  <thead>
      <tr>
          <th>屬性</th>
          <th>Provisioned</th>
          <th>Serverless v2</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>容量設定</td>
          <td>固定 instance class（如 db.r6g.xlarge）</td>
          <td>min / max ACU 區間</td>
      </tr>
      <tr>
          <td>計費</td>
          <td>按 instance 開機時數</td>
          <td>按實際消耗的 ACU-秒</td>
      </tr>
      <tr>
          <td>擴縮</td>
          <td>手動改 instance class（有中斷）</td>
          <td>秒級自動伸縮、無中斷</td>
      </tr>
      <tr>
          <td>離峰成本</td>
          <td>付滿整台</td>
          <td>縮到 min ACU、只付低水位</td>
      </tr>
      <tr>
          <td>適用負載</td>
          <td>穩定、可預測</td>
          <td>間歇、突發、難預測</td>
      </tr>
  </tbody>
</table>
<p><strong>擴縮行為</strong>：</p>
<ul>
<li>負載上升時 ACU 平滑增加、不需要切換 instance、無連線中斷</li>
<li>負載下降時縮回低水位、但受 min ACU 下限約束</li>
<li>min ACU 決定離峰的最低成本與「保留多少暖容量」；max ACU 決定尖峰的上限與成本天花板</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「ACU 對應的記憶體比例」「serverless v2 是否能縮到 0」「最小 ACU 粒度」這些屬 AWS vendor 規格、會隨版本演進（auto-pause 等能力陸續調整）、實作時 cross-verify 官方 doc 當前值。本文不含 production case 揭露的 ACU 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/peak-forecast/" data-link-title="Peak Forecast" data-link-desc="說明預期峰值流量的預測方法 — 容量規劃的第一個輸入">peak forecast</a>、<a href="/blog/backend/knowledge-cards/cost-per-request/" data-link-title="Cost Per Request" data-link-desc="把雲端成本拆到單一 API 請求的 unit economics 模型">cost per request</a>。</p>
<h2 id="min--max-acu-的設定權衡">min / max ACU 的設定權衡</h2>
<p>min 與 max ACU 不是隨便設，兩端各自承擔不同風險。</p>
<p><strong>min ACU 太低</strong>：離峰省錢，但流量回升時從很低的水位往上爬、爬升期間可能容量不足、且 buffer cache 在低 ACU 時被壓縮、回升後 cache 重新暖機、query latency 短暫升高。對延遲敏感、又有規律日週期的 workload，min ACU 不要壓到極限。</p>
<p><strong>max ACU 太低</strong>：尖峰被天花板卡住、等同 provisioned 的 instance class 上限問題又回來。max ACU 要按「預期尖峰 + 餘量」設，並把它當成成本天花板來監控——max 設太高雖然不會平時就花錢，但失控 query（如缺索引的全表掃描）可能把 ACU 一路推到 max、帳單尖峰。</p>
<p><strong>暖容量考量</strong>：min ACU 同時決定「保留多少隨時可用的暖容量」。完全不可預測、且要求第一個請求就低延遲的場景，min ACU 要留足暖機水位，不能為了省錢設到最低。</p>
<h2 id="混合-clusterserverless--provisioned-並存">混合 cluster：serverless + provisioned 並存</h2>
<p>Serverless v2 不是「整個 cluster 要嘛全 serverless、要嘛全 provisioned」。同一個 Aurora cluster 可以混用：writer 用 provisioned 保穩定、read replica 用 serverless v2 吸收讀取尖峰；或反過來。這讓 workload 的不同部分各取所需：</p>
<ul>
<li>穩定的寫入路徑用 provisioned instance、成本可預測</li>
<li>間歇的讀取分析、報表副本用 serverless v2、平時縮到低水位</li>
<li>failover 目標可指定 provisioned 或 serverless，依可用性需求</li>
</ul>
<p>混合配置的判讀是把 cluster 內每個角色當獨立的負載形狀評估，而非整個 cluster 一刀切。</p>
<h2 id="操作流程">操作流程</h2>
<p>從負載形狀評估到上線的 6 步流程。</p>
<h4 id="step-1判斷負載形狀">Step 1：判斷負載形狀</h4>
<p>用 CloudWatch 過去 30 天的 CPU / connection / IOPS，看負載是穩定平緩、規律日週期、還是不規則突發：</p>
<ul>
<li>穩定高負載（平均使用率高、波動小）→ provisioned 通常更划算</li>
<li>間歇 / 突發 / 開發測試 / 多租戶各自小 DB → serverless v2 適合</li>
<li>規律日週期（白天高晚上低）→ serverless v2 或 provisioned + scheduled 都可，算成本 crossover</li>
</ul>
<h4 id="step-2估-min--max-acu">Step 2：估 min / max ACU</h4>
<p>min 依離峰最低負載 + 暖容量需求；max 依尖峰負載 + 餘量。第一次設保守一點、上線後依實際 ACU 曲線收斂。</p>
<h4 id="step-3建立或轉換">Step 3：建立或轉換</h4>





<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"><span class="c1"># 新 cluster 指定 serverless v2 capacity range</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws rds create-db-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --db-cluster-identifier my-cluster <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --engine aurora-postgresql <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --serverless-v2-scaling-configuration <span class="nv">MinCapacity</span><span class="o">=</span>2,MaxCapacity<span class="o">=</span><span class="m">32</span></span></span></code></pre></div><p>既有 provisioned cluster 可加 serverless v2 reader、逐步驗證再調整 writer。</p>
<h4 id="step-4觀察-acu-曲線">Step 4：觀察 ACU 曲線</h4>
<p>上線後盯 <code>ServerlessDatabaseCapacity</code>（即時 ACU）與 <code>ACUUtilization</code>，確認伸縮符合負載、min/max 設定合理。</p>
<h4 id="step-5成本對照">Step 5：成本對照</h4>
<p>把實際 ACU-秒換算的帳單，跟「同等 provisioned instance 全時段開機」對照。若 serverless 帳單接近或超過 provisioned，代表負載其實夠穩定、該回 provisioned。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<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"># 驗證離峰真的縮到 min ACU（看 ServerlessDatabaseCapacity 低谷）
</span></span><span class="line"><span class="ln">2</span><span class="cl"># 驗證尖峰沒撞 max ACU 天花板（看是否長時間貼著 max）
</span></span><span class="line"><span class="ln">3</span><span class="cl"># 驗證回升期 latency 可接受（min ACU 暖容量是否足夠）</span></span></code></pre></div><p><strong>Rollback boundary</strong>：serverless v2 與 provisioned 可互轉、reader 先轉驗證再動 writer；轉換本身有短暫中斷，要排 maintenance window。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1穩定高負載用-serverless-反而更貴">Case 1：穩定高負載用 serverless 反而更貴</h4>
<p>把一個 7x24 高使用率的 cluster 改 serverless「以為省錢」，實際 ACU 幾乎全時段貼近高水位、按 ACU-秒計費比固定 instance 貴。修法：穩定高負載用 provisioned；serverless 的省錢前提是「有顯著的離峰可以縮」。</p>
<h4 id="case-2min-acu-設太低回升期-latency-尖刺">Case 2：min ACU 設太低、回升期 latency 尖刺</h4>
<p>離峰縮到極低、早上流量回來時 cache 冷、ACU 從低水位爬、前幾分鐘 query 變慢。修法：規律日週期的 workload，min ACU 留足暖容量；或用 provisioned + scheduled scaling 處理可預測的日週期。</p>
<h4 id="case-3max-acu-沒當成本天花板監控">Case 3：max ACU 沒當成本天花板監控</h4>
<p>缺索引的 query 觸發全表掃描、ACU 一路衝到 max、帳單尖峰才發現。修法：max ACU 設合理上限 + CloudWatch alarm 盯 ACU 長時間貼 max（那是 query 或容量問題的訊號，不是正常擴縮）。</p>
<h4 id="case-4把-serverless-當不用做容量規劃">Case 4：把 serverless 當「不用做容量規劃」</h4>
<p>以為 serverless 自動伸縮就不必估容量、min/max 隨便設。修法：serverless 改變的是「不用手動切 instance」，不是「不用理解負載形狀」；min/max 仍要基於負載曲線設定。</p>
<h4 id="case-5對延遲極敏感的-oltp-全-serverless">Case 5：對延遲極敏感的 OLTP 全 serverless</h4>
<p>核心交易路徑要求穩定低延遲、卻用會伸縮的 serverless writer、伸縮邊界期間 latency 抖動。修法：穩定低延遲的核心寫入用 provisioned writer，serverless 留給可容忍伸縮抖動的讀取 / 分析副本（混合 cluster）。</p>
<p><strong>Anti-recommendation</strong>：負載穩定、使用率長期偏高、或對延遲抖動零容忍的核心 OLTP → 用 provisioned；serverless v2 的價值在「間歇、突發、難預測、或有大量離峰」的負載，沒有離峰可縮就沒有省錢空間。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ServerlessDatabaseCapacity</code>：即時 ACU、看伸縮曲線</li>
<li><code>ACUUtilization</code>：ACU 使用率、判斷 min/max 設定是否合理</li>
<li><code>CPUUtilization</code> / <code>DatabaseConnections</code>：底層負載、對照 ACU 是否跟得上</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li>ACU 長時間貼近 max → max 設太低或有失控 query，要查</li>
<li>ACU 長時間貼近 min 且使用率低 → 負載其實很輕，min 可能可再降、或這個 cluster 適合更小配置</li>
<li>ACU 幾乎不波動且水位高 → 負載穩定，serverless 沒發揮價值，評估改 provisioned</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 ACU 數字；上述 metric 與判讀屬 vendor 規格 + 通用容量工程。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora 容量規劃要點</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="serverless-v2-vs-provisioned--scheduled-scaling">Serverless v2 vs provisioned + scheduled scaling</h3>
<p>兩者都能處理「負載隨時間變」，但適用場景不同：</p>
<ul>
<li><strong>scheduled scaling（provisioned）</strong>：負載 <em>可預測</em>（已知的日週期、已知大活動）→ 預先排程改容量，成本最可控</li>
<li><strong>serverless v2</strong>：負載 <em>不可預測</em>（突發、不規則）→ 自動伸縮吸收，不需預測</li>
</ul>
<p>可預測的尖峰用 scheduled、不可預測的用 serverless，這跟 <a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">DynamoDB capacity mode</a> 的 predictable-peak vs flash-sale 判讀同源。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/aurora/storage-architecture/" data-link-title="Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計" data-link-desc="Aurora storage / compute 分離、6-way 跨 AZ replication、4-of-6 write / 3-of-6 read quorum、韌性投資自動 amortize 成 read 性能、DraftKings 6ms 寫 / &lt;1ms 讀 production reference">storage-architecture</a> — serverless 只改 compute 層容量、storage 層 quorum 設計不變</li>
<li><a href="/blog/backend/01-database/vendors/aurora/read-replica-scaling/" data-link-title="Aurora Read Replica Scaling：15 replica 上限、lag profile、headroom 預留與 fleet 治理" data-link-desc="Aurora 15 replica 上限、共享 storage 為什麼能養大量 replica、事件型容量分級表、DraftKings headroom 預留判讀、FanDuel 雙 SLO 並行、fleet 治理 3 條 driver（business sharding / microservice / 合規）">read-replica-scaling</a> — serverless reader 吸收讀取尖峰、與 fleet 治理結合</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/aurora-io-optimized-cost/" data-link-title="Aurora PostgreSQL I/O-Optimized Cost" data-link-desc="Aurora PostgreSQL Standard 與 I/O-Optimized 的成本模型、I/O 壓力、workload 判斷、遷移與回退條件">Aurora I/O-Optimized cost</a> — serverless 算的是 compute（ACU）成本、I/O-Optimized 算的是 storage I/O 成本，兩個成本軸獨立、要分開評估</li>
<li><a href="/blog/backend/01-database/vendors/aurora/rds-proxy-connection-pooling/" data-link-title="Aurora RDS Proxy 與連線管理：connection multiplexing、pinning 陷阱與 failover 加速" data-link-desc="RDS Proxy 不是「連上去就自動省連線」；本文展開 connection multiplexing 機制、哪些 session 操作會觸發 pinning 讓 multiplexing 失效、failover 期間 proxy 如何保持 client 連線縮短中斷，以及 RDS Proxy 與自管 pgbouncer 的責任切分">rds-proxy-connection-pooling</a> — serverless + Lambda 場景的連線管理</li>
<li>替代路由：負載穩定且高 → provisioned；KV access pattern → <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a></li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/netflix-aurora-consolidation/" data-link-title="9.C23 Netflix：把關聯式 DB 統一到 Aurora、效能 &#43;75%、成本 -28%" data-link-desc="Netflix 把多套關聯式 DB 統一到 Aurora、效能提升 75%、成本下降 28%、串流數十億小時">Netflix 9.C23</a> 互引：polyglot 架構下不同 workload 用不同 Aurora 配置（穩定 OLTP provisioned、間歇副本 serverless）</li>
</ul>
]]></content:encoded></item><item><title>Aurora PostgreSQL I/O-Optimized Cost</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/aurora-io-optimized-cost/</link><pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/postgresql/aurora-io-optimized-cost/</guid><description>&lt;p>Aurora PostgreSQL I/O-Optimized cost 的核心責任是把 Aurora storage configuration 從定價選項轉成 workload 決策。AWS 官方文件將 Aurora cluster storage configuration 分成 Aurora Standard 與 Aurora I/O-Optimized；前者適合一般 I/O 分布，後者針對 I/O 密集 workload 提供不同成本結構。&lt;/p>
&lt;p>本文的判讀錨點是：I/O-Optimized 是成本與 workload profile 決策，而非效能保證。要看的是 read / write I/O charge、storage、instance、backup、replica、query pattern、maintenance 與未來成長。&lt;/p>
&lt;p>官方文件路由的核心責任是固定時間敏感 claim。實作前先查 &lt;a href="https://docs.aws.amazon.com/en_us/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html">Aurora storage configurations&lt;/a> 與 &lt;a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.Aurora_Fea_Regions_DB-eng.Feature.storage-type.html">supported engines / regions&lt;/a>；本文最後檢查日是 2026-05-22。&lt;/p>
&lt;h2 id="cost-model">Cost Model&lt;/h2>
&lt;p>Cost model 的核心責任是拆解 Aurora bill 的來源。Aurora 成本通常包含 instance、storage、I/O request、backup、replica、data transfer 與 support / operation。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>成本項&lt;/th>
 &lt;th>Standard 判讀&lt;/th>
 &lt;th>I/O-Optimized 判讀&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Instance&lt;/td>
 &lt;td>仍依 instance / capacity 計費&lt;/td>
 &lt;td>仍依 instance / capacity 計費&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Storage&lt;/td>
 &lt;td>依儲存使用量&lt;/td>
 &lt;td>依 I/O-Optimized storage 設定&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>I/O requests&lt;/td>
 &lt;td>I/O 成本可成為主要變動項&lt;/td>
 &lt;td>I/O charge 結構改變，適合高 I/O workload&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Backup / snapshot&lt;/td>
 &lt;td>依保留與使用量&lt;/td>
 &lt;td>仍需納入總成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data transfer&lt;/td>
 &lt;td>跨 AZ / region / service 需審查&lt;/td>
 &lt;td>仍需納入總成本&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>成本評估要用真實帳單和 CloudWatch 指標。只用平均 QPS 估算會漏掉 batch job、vacuum、index build、replica、backfill 與報表查詢帶來的 I/O 尖峰。&lt;/p>
&lt;h2 id="workload-signals">Workload Signals&lt;/h2>
&lt;p>Workload signals 的核心責任是找出 I/O 是否為主要成本與瓶頸。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>意義&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>I/O request 成本占比高&lt;/td>
 &lt;td>Standard 可能受 I/O charge 影響大&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Buffer cache hit ratio 低&lt;/td>
 &lt;td>工作集超過 memory 或 query 掃描過重&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>大量 random read / write&lt;/td>
 &lt;td>storage I/O 壓力明顯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ETL / backfill 經常跑&lt;/td>
 &lt;td>短期 I/O spike 可能影響帳單與 latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Index / query 設計已優化&lt;/td>
 &lt;td>成本切換更能反映真實 workload&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>先做 query 與 index review。若 I/O 來自缺 index、全表掃描、過度 eager loading 或不必要 backfill，直接切 I/O-Optimized 只會把浪費制度化。&lt;/p>
&lt;h2 id="evaluation-process">Evaluation Process&lt;/h2>
&lt;p>Evaluation process 的核心責任是讓切換決策可回溯。&lt;/p>
&lt;ol>
&lt;li>收集 30 到 90 天成本：instance、storage、I/O、backup、transfer。&lt;/li>
&lt;li>收集 workload 指標：read/write IOPS、cache hit、slow query、top SQL。&lt;/li>
&lt;li>標記特殊事件：migration、backfill、incident、seasonality。&lt;/li>
&lt;li>建立 Standard vs I/O-Optimized 成本試算。&lt;/li>
&lt;li>在 staging / canary 確認 application behavior。&lt;/li>
&lt;li>設定切換後 7 / 14 / 30 天回顧點。&lt;/li>
&lt;/ol>
&lt;p>試算要包含季節性。月初結算、年度促銷、批次報表與資料重整都可能讓 I/O profile 和普通週不同。&lt;/p></description><content:encoded><![CDATA[<p>Aurora PostgreSQL I/O-Optimized cost 的核心責任是把 Aurora storage configuration 從定價選項轉成 workload 決策。AWS 官方文件將 Aurora cluster storage configuration 分成 Aurora Standard 與 Aurora I/O-Optimized；前者適合一般 I/O 分布，後者針對 I/O 密集 workload 提供不同成本結構。</p>
<p>本文的判讀錨點是：I/O-Optimized 是成本與 workload profile 決策，而非效能保證。要看的是 read / write I/O charge、storage、instance、backup、replica、query pattern、maintenance 與未來成長。</p>
<p>官方文件路由的核心責任是固定時間敏感 claim。實作前先查 <a href="https://docs.aws.amazon.com/en_us/AmazonRDS/latest/AuroraUserGuide/Aurora.Overview.StorageReliability.html">Aurora storage configurations</a> 與 <a href="https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/Concepts.Aurora_Fea_Regions_DB-eng.Feature.storage-type.html">supported engines / regions</a>；本文最後檢查日是 2026-05-22。</p>
<h2 id="cost-model">Cost Model</h2>
<p>Cost model 的核心責任是拆解 Aurora bill 的來源。Aurora 成本通常包含 instance、storage、I/O request、backup、replica、data transfer 與 support / operation。</p>
<table>
  <thead>
      <tr>
          <th>成本項</th>
          <th>Standard 判讀</th>
          <th>I/O-Optimized 判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Instance</td>
          <td>仍依 instance / capacity 計費</td>
          <td>仍依 instance / capacity 計費</td>
      </tr>
      <tr>
          <td>Storage</td>
          <td>依儲存使用量</td>
          <td>依 I/O-Optimized storage 設定</td>
      </tr>
      <tr>
          <td>I/O requests</td>
          <td>I/O 成本可成為主要變動項</td>
          <td>I/O charge 結構改變，適合高 I/O workload</td>
      </tr>
      <tr>
          <td>Backup / snapshot</td>
          <td>依保留與使用量</td>
          <td>仍需納入總成本</td>
      </tr>
      <tr>
          <td>Data transfer</td>
          <td>跨 AZ / region / service 需審查</td>
          <td>仍需納入總成本</td>
      </tr>
  </tbody>
</table>
<p>成本評估要用真實帳單和 CloudWatch 指標。只用平均 QPS 估算會漏掉 batch job、vacuum、index build、replica、backfill 與報表查詢帶來的 I/O 尖峰。</p>
<h2 id="workload-signals">Workload Signals</h2>
<p>Workload signals 的核心責任是找出 I/O 是否為主要成本與瓶頸。</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>意義</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>I/O request 成本占比高</td>
          <td>Standard 可能受 I/O charge 影響大</td>
      </tr>
      <tr>
          <td>Buffer cache hit ratio 低</td>
          <td>工作集超過 memory 或 query 掃描過重</td>
      </tr>
      <tr>
          <td>大量 random read / write</td>
          <td>storage I/O 壓力明顯</td>
      </tr>
      <tr>
          <td>ETL / backfill 經常跑</td>
          <td>短期 I/O spike 可能影響帳單與 latency</td>
      </tr>
      <tr>
          <td>Index / query 設計已優化</td>
          <td>成本切換更能反映真實 workload</td>
      </tr>
  </tbody>
</table>
<p>先做 query 與 index review。若 I/O 來自缺 index、全表掃描、過度 eager loading 或不必要 backfill，直接切 I/O-Optimized 只會把浪費制度化。</p>
<h2 id="evaluation-process">Evaluation Process</h2>
<p>Evaluation process 的核心責任是讓切換決策可回溯。</p>
<ol>
<li>收集 30 到 90 天成本：instance、storage、I/O、backup、transfer。</li>
<li>收集 workload 指標：read/write IOPS、cache hit、slow query、top SQL。</li>
<li>標記特殊事件：migration、backfill、incident、seasonality。</li>
<li>建立 Standard vs I/O-Optimized 成本試算。</li>
<li>在 staging / canary 確認 application behavior。</li>
<li>設定切換後 7 / 14 / 30 天回顧點。</li>
</ol>
<p>試算要包含季節性。月初結算、年度促銷、批次報表與資料重整都可能讓 I/O profile 和普通週不同。</p>
<h2 id="migration-and-rollback">Migration and Rollback</h2>
<p>Migration and rollback 的核心責任是把 storage configuration change 放進變更流程。Aurora storage configuration 是 cluster-level decision，應先確認支援區域、engine version、切換限制、維護窗口與回退條件。</p>
<table>
  <thead>
      <tr>
          <th>Step</th>
          <th>Evidence</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Pre-check</td>
          <td>engine version、region support、current bill</td>
      </tr>
      <tr>
          <td>Cost baseline</td>
          <td>近期成本與 I/O 指標</td>
      </tr>
      <tr>
          <td>Change window</td>
          <td>application traffic、maintenance</td>
      </tr>
      <tr>
          <td>Post-check</td>
          <td>latency、I/O、error、bill trend</td>
      </tr>
      <tr>
          <td>Review</td>
          <td>7 / 14 / 30 天成本與效能</td>
      </tr>
  </tbody>
</table>
<p>Rollback 條件要明確。若切換後成本下降未達目標、latency 沒改善、或 workload profile 改變，應重新評估 Standard 與 query optimization。</p>
<h2 id="anti-patterns">Anti-Patterns</h2>
<p>Anti-pattern 的核心責任是避免把計費選項當成效能調校。</p>
<table>
  <thead>
      <tr>
          <th>反模式</th>
          <th>風險</th>
          <th>修正方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>未看 top SQL 直接切換</td>
          <td>把壞 query 的成本包進新方案</td>
          <td>先做 query / index review</td>
      </tr>
      <tr>
          <td>用單日帳單推估全年</td>
          <td>忽略 seasonality</td>
          <td>至少看完整業務週期</td>
      </tr>
      <tr>
          <td>忽略 backup / transfer</td>
          <td>總成本估算失真</td>
          <td>全 bill component 一起比較</td>
      </tr>
      <tr>
          <td>切換後無 review</td>
          <td>成本漂移無 owner</td>
          <td>設定 7 / 14 / 30 天 tripwire</td>
      </tr>
  </tbody>
</table>
<p>I/O-Optimized 的價值來自成本結構對齊 workload。它應該是 FinOps 與 database operation 的共同決策。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>Aurora I/O-Optimized cost 完成後，Aurora 遷移讀 <a href="../migrate-to-aurora/">PostgreSQL to Aurora Migration</a>；query 成本讀 <a href="../query-optimization/">Query Optimization</a>；capacity 與瓶頸判斷讀 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">Bottleneck Localization</a>。</p>
]]></content:encoded></item></channel></rss>